diff --git a/plugins/janvince/smallcontactform/LICENCE.md b/plugins/janvince/smallcontactform/LICENCE.md
new file mode 100644
index 000000000..727b0403f
--- /dev/null
+++ b/plugins/janvince/smallcontactform/LICENCE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Jan Vince
+
+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.
diff --git a/plugins/janvince/smallcontactform/Plugin.php b/plugins/janvince/smallcontactform/Plugin.php
new file mode 100644
index 000000000..53fcaccc8
--- /dev/null
+++ b/plugins/janvince/smallcontactform/Plugin.php
@@ -0,0 +1,213 @@
+ 'janvince.smallcontactform::lang.plugin.name',
+ 'description' => 'janvince.smallcontactform::lang.plugin.description',
+ 'author' => 'Jan Vince',
+ 'icon' => 'icon-inbox'
+ ];
+ }
+
+ public function boot() {
+
+ /**
+ * Custom Validator rules
+ */
+ Validator::extend('custom_not_regex', function ($attribute, $value, $parameters) {
+
+ if (is_array($parameters)) {
+ $param = $parameters[0];
+ } else {
+ $param = $parameters;
+ }
+
+ try {
+ $result = preg_match($param, $value);
+
+ if ($result === 1) {
+ return false;
+ } else {
+ return true;
+ }
+ } catch (\Exception $e) {
+ Log::error('Error in Small Contact Form custom_not_regex validation rule! ' . $e->getMessage());
+ }
+
+ return false;
+ });
+
+ }
+
+ public function registerSettings() {
+
+ return [
+ 'settings' => [
+ 'label' => 'janvince.smallcontactform::lang.plugin.name',
+ 'description' => 'janvince.smallcontactform::lang.plugin.description',
+ 'category' => 'Small plugins',
+ 'icon' => 'icon-inbox',
+ 'class' => 'JanVince\SmallContactForm\Models\Settings',
+ 'keywords' => 'small contact form message recaptcha antispam',
+ 'order' => 990,
+ 'permissions' => ['janvince.smallcontactform.access_settings'],
+ ]
+ ];
+ }
+
+ public function registerNavigation(){
+ return [
+ 'smallcontactform' => [
+ 'label' => 'janvince.smallcontactform::lang.navigation.main_label',
+ 'url' => Backend::url('janvince/smallcontactform/messages'),
+ 'icon' => 'icon-inbox',
+ 'permissions' => ['janvince.smallcontactform.access_messages'],
+ 'order' => 990,
+
+ 'sideMenu' => [
+ 'messages' => [
+ 'label' => 'janvince.smallcontactform::lang.navigation.messages',
+ 'icon' => 'icon-envelope-o',
+ 'url' => Backend::url('janvince/smallcontactform/messages'),
+ 'permissions' => ['janvince.smallcontactform.access_messages']
+ ],
+
+ ],
+
+ ],
+
+ ];
+
+ }
+
+ public function registerPermissions(){
+
+ return [
+ 'janvince.smallcontactform.access_messages' => [
+ 'label' => 'janvince.smallcontactform::lang.permissions.access_messages',
+ 'tab' => 'janvince.smallcontactform::lang.plugin.name',
+ ],
+ 'janvince.smallcontactform.access_settings' => [
+ 'label' => 'janvince.smallcontactform::lang.permissions.access_settings',
+ 'tab' => 'janvince.smallcontactform::lang.plugin.name',
+ ],
+ 'janvince.smallcontactform.delete_messages' => [
+ 'label' => 'janvince.smallcontactform::lang.permissions.delete_messages',
+ 'tab' => 'janvince.smallcontactform::lang.plugin.name',
+ ],
+ 'janvince.smallcontactform.export_messages' => [
+ 'label' => 'janvince.smallcontactform::lang.permissions.export_messages',
+ 'tab' => 'janvince.smallcontactform::lang.plugin.name',
+ ],
+ ];
+
+ }
+
+ public function registerComponents()
+ {
+ return [
+ 'JanVince\SmallContactForm\Components\SmallContactForm' => 'contactForm',
+ ];
+ }
+
+ public function registerPageSnippets()
+ {
+ return [
+ 'JanVince\SmallContactForm\Components\SmallContactForm' => 'contactForm',
+ ];
+ }
+
+ public function registerMailTemplates()
+ {
+
+ return Settings::getTranslatedTemplates();
+
+ }
+
+ public function registerMarkupTags()
+ {
+ return [
+ 'filters' => [
+ ],
+ 'functions' => [
+ 'trans' => function($value) { return e(trans($value)); },
+ 'html_entity_decode' => function($value) { return html_entity_decode($value); },
+ 'settingsGet' => function($value, $default = NULL) { return Settings::get($value, $default); }
+ ]
+ ];
+ }
+
+ /**
+ * Custom list types
+ */
+ public function registerListColumnTypes()
+ {
+
+
+ return [
+ 'strong' => function($value) { return ''. $value . ' '; },
+ 'text_preview' => function($value) { $content = mb_substr(strip_tags($value), 0, 150); if(mb_strlen($content) > 150) { return ($content . '...'); } else { return $content; } },
+ 'array_preview' => function($value) { $content = mb_substr(strip_tags( implode(' --- ', $value) ), 0, 150); if(mb_strlen($content) > 150) { return ($content . '...'); } else { return $content; } },
+ 'switch_icon_star' => function($value) { return '
' . ($value==1 ? e(trans('janvince.smallcontactform::lang.models.message.columns.new')) : e(trans('janvince.smallcontactform::lang.models.message.columns.read')) ) . '
'; },
+ 'switch_extended_input' => function($value) { if($value){return ' ';} else { return ' ';} },
+ 'switch_extended' => function($value) { if($value){return ' ';} else { return ' ';} },
+ 'attached_images_count' => function($value){ return (count($value) ? count($value) : NULL); },
+ 'image_preview' => function($value) {
+ $width = Settings::get('records_list_preview_width') ? Settings::get('records_list_preview_width') : 50;
+ $height = Settings::get('records_list_preview_height') ? Settings::get('records_list_preview_height') : 50;
+
+ if($value){ return " "; }
+ },
+ 'scf_files_link' => function($value){
+ if(!empty($value)) {
+ $output = [];
+ foreach($value as $file) {
+ $output[] = "";
+ }
+ return implode('', $output);
+ }
+ },
+ ];
+ }
+
+ public function registerReportWidgets()
+ {
+ return [
+ 'JanVince\SmallContactForm\ReportWidgets\Messages' => [
+ 'label' => 'janvince.smallcontactform::lang.reportwidget.partials.messages.label',
+ 'context' => 'dashboard'
+ ],
+ 'JanVince\SmallContactForm\ReportWidgets\NewMessage' => [
+ 'label' => 'janvince.smallcontactform::lang.reportwidget.partials.new_message.label',
+ 'context' => 'dashboard'
+ ],
+ ];
+ }
+
+}
diff --git a/plugins/janvince/smallcontactform/README.md b/plugins/janvince/smallcontactform/README.md
new file mode 100644
index 000000000..129f35368
--- /dev/null
+++ b/plugins/janvince/smallcontactform/README.md
@@ -0,0 +1,440 @@
+# Small Contact form
+> Simple but flexible contact form builder with custom fields, validation and passive antispam.
+
+
+## Installation
+
+**GitHub** clone into `/plugins` dir:
+
+```sh
+git clone https://github.com/jan-vince/smallcontactform
+```
+
+**OctoberCMS backend**
+
+Just look for 'Small Contact Form' in search field in:
+> Settings > Updates & Plugins > Install plugins
+
+### Permissions
+
+> Settings > Administrators
+
+You can set permissions to restrict access to *Settings > Small plugins > Contact form* and to messages list.
+
+
+### Installation with composer
+
+* Edit composer.json by adding new repository
+```
+"repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/jan-vince/smallcontactform"
+ }
+]
+```
+* run in command line
+```sh
+composer require janvince/smallcontactform
+```
+
+
+## Setup new Contact form
+
+> Settings > Small Contact form
+
+### FORM
+
+* You can set your own CSS class name and general success/error messages.
+* If you need it, placeholders can be used instead of labels
+* Form can be hidden after successful submit.
+
+
+#### Enable AJAX
+
+By default, sending form will trigger page reload. With AJAX, everything can be done without page reloading which will be more user friendly.
+*If user's browser doesn't support (or has disabled) JavaScript, form will still work with page reloads after send.*
+
+* For AJAX enabled form, before send confirmation dialog can be required.
+
+
+#### Add Assets
+
+If you want to start quickly, you can enable Add assets checkbox - and then Add CSS and JS assets.
+This will include necessary styles (Bootstrap, AJAX, October AJAX) and scripts (jQuery, Bootstrap, October AJAX framework and extras).
+
+But you have to include Twig tags ````{% styles %}```` and ````{% scripts %}```` into your layout or page like this:
+
+````
+
+
+ {% styles %}
+
+
+
+ {% page %}
+
+ {% scripts %}
+
+
+
+
+````
+
+If you want to insert assets by hand, you can do it this way (or similar):
+
+````
+
+
+
+
+
+
+ {% page %}
+
+
+
+
+
+
+````
+
+### SEND BUTTON
+
+* You can set button class and text.
+
+#### Redirection after the form is sent
+
+You have some options to control redirection after form is successfully sent:
+
+* In main form settings you can allow redirection and set fixed URL (internal or external)
+* In component properties (on CMS Page or Layout) you can override main redirection settings for a specific form
+* You can add a dynamic redirect URL as a markup parameter eg. `{% component 'contactForm' redirect_url = ('/success#'~this.page.id) %}`
+
+> If you use markup parameter do not forget to allow form redirection in form main settings or (rather) in component parameters ! There is no markup parametr to allow redirection.
+
+
+### FIELDS
+
+Here you can add fields to build your contact (or other) form.
+
+The idea is simple (and solution is so I hope):
+
+* Click to add new field
+ * Set it's name (this is used for ```` ````), so it should be lowercase without special characters.
+ * Set Label if you need one (it is used for descriptive text above input field)
+ * Set autofocus if you want cursor to automatically jump to this field (if checked more than one field, cursor jumps to first one)
+
+When dropdown is selected there will be values/options repeater shown. You can add as many values you need.
+
+> Hint: you can add dropdown empty option by adding a value with empty ID.
+
+You can also use **Custom code** and have complete control of generated code.
+
+There is also a **Custom content** field to add formated content in place of a field.
+
+#### Field data validation
+
+You can select from predefined rules or add custom Validator rules (read [documentation](https://octobercms.com/docs/services/validation#available-validation-rules)).
+
+Some rules require additional validation pattern some of them not.
+
+ * You can add one or more validation rules and error messages for them
+ * Error messages will be shown above input field
+ * You can reorder fields by drag and drop left circle (all fields can be collapsed by pressing Ctrl+click (Cmd+click on MacOS) on arrow in right top corners)
+
+> Hint: For dropdown validation you can use `custom` validation type with rule `in` and list of IDs in `pattern` field (eg: 1,2,3).
+
+> Note: There is a `custom_not_regex` validation rule as an inverse to built in `regex`.
+
+### COLUMNS MAPPING
+
+System writes all form data in database, but for quick overview Name, Email and Message columns are visible separately in Messages list.
+
+But you have to help system to identify these columns by mapping to your form fields.
+
+These mappings are also used for autoreply emails where at least Email field mapping is important.
+
+
+### ANTISPAM
+
+#### Passive antispam
+
+Very simple implementation of passive antispam (inspired by [Nette AntiSpam Control](https://gist.github.com/Michal-Mikolas/2388131)).
+
+The idea behind this is to check how fast is form send and if robots-catching field is filled.
+
+* When allowed, you can set form delay (in seconds) to prevent too fast form sending (mostly by robots). You can add custom error message (will be shown in general error message box above form).
+* You can add antispam field label and error message for non JavaScript enabled browsers.
+ * If JavaScript is working, antispam field is automatically hidden and cleared.
+
+#### Google reCaptcha
+
+Implementation of Google reCaptcha antispam protection.
+
+##### Setup
+
+First you have to create new API keys pair in reCaptcha admin panel.
+
+Hit **Get reCAPTCHA** button on [reCaptcha wellcome page](https://www.google.com/recaptcha). Set label and check reCAPTCHA v2 option and hit button Register.
+
+Copy Site key and Secret key to Contact Form's settings fields.
+
+If you want Contact Form to automatically include server scripts in your layout, check the button in Form settings.
+
+#### Check sender's IP
+
+You can add an extra form protection with limit submits from one IP address.
+
+This check has own error message and custom field to set maximum submits.
+
+
+### EMAIL
+
+Mails can be sent directly or queued ([OctoberCMS queue](https://octobercms.com/docs/services/queues) must be configured!).
+
+Don't forget to configure mail preferences in *Settings > Mail > Mail configuration*!
+
+#### Data in email templates
+
+There are variables available in all email templates:
+
+* **fields** is array of [ 'field name' => 'post value' ]
+* **fieldsDetails** is array of [ 'field name' => ['name', 'value', 'type', ...] ]
+
+* **uploads** is array of uploads (of class `System\Models\File`)
+
+* **messageObject** is a model instance of a selected message
+
+#### Allow autoreply
+
+Email can be send to form sender as confirmation.
+
+* You have to enter email address and name - it will be used as FROM field
+* Email subject can be manually added here (or edited in *Settings > Mail > Mail templates (code: janvince.smallcontactform::mail.autoreply)*)
+* Email TO address and name have to be assigned to form fields (in selections only corresponding field types are shown - if you don't see one, try to check it's type in Fields tab)
+* Email REPLY TO address can be set
+* Message field can be also assigned (and will be saved separately into database)
+
+#### Allow notifications
+
+Once a Contact form is sent a notification can be immediately send to a provided email address (or comma-separated list of addresses).
+
+*A **Reply to** address of notification email will be set to an email address from Contact form (if this field is used).*
+
+You can also force **From** address to be set to the one entered in Contact form - but not all email systems support this!
+
+
+## TRANSLATION
+
+You can allow translation with [RainLab Translate](https://octobercms.com/plugin/rainlab-translate) plugin.
+
+> After installation of Translate plugin, please add at least two languages in *Settings > Translate > Manage languages*.
+> For translations to work there must be a localePicker component included in your layout/page.
+
+#### Form texts
+
+Most of Small Contact form texts can be edited right in *Settings > Small plugins > Contact form*.
+
+#### Custom form fields
+
+Translate plugin doesn't supports translation of individual repeater fields yet, so form field texts (label, validation error messages) have to be - for now - translated in a dictionary: *Settings > Translate > Translate messages*
+
+> Please note that form fields labels will be shown in dictionary after first form render (on your frontend page) and validation error messages after first send.
+
+#### Email templates
+
+You can create your own email templates in *Settings > Mail > Mail templates* (for hint look inside of default templates starting with *janvince.smallcontactform::*).
+
+Remember your email templates CODE and put in in Small Contact form email settings in *Settings > Small plugins > Contact form > Email tab*. For each language there can be specific template.
+
+There are `{{fields}}` and `{{fieldsDetails}}` arrays available inside of email templates.
+
+You can also use `{{url}}` variable to get original request URL.
+
+*If your custom form field has name eg. 'email', you use it in template with ````{{fields.email}}````.*
+
+You can itterate over uploaded files with:
+```
+{% for item in uploads %}
+ Uploaded file
+{% endfor %}
+```
+
+You can access model data with eg. `{{ messageObject.id }}`.
+
+## GOOGLE ANALYTICS
+
+> if you want to use these settings, be sure to have Google Analytics scripts included on your site. You can use [Rainlab Google Analytics plugin](https://octobercms.com/plugin/rainlab-googleanalytics).
+
+### Events
+
+You can allow events to be send to your GA account when the form is successfully sent.
+
+There are (translatable) fields for category, action and label.
+
+*All event settings can be overriden in component property so if you use more then one form, you can custommize events for each of them.*
+
+
+## MESSAGES LIST
+
+All sent data from Contact form are saved and listed in backend Messages list.
+
+If email, name and message fields are assigned on *Settings > Small plugins > contact form > Columns mapping tab*, they will be saved and shown in separate columns.
+
+You can click on a record to see all form data. The message will be marked as read.
+
+
+## DASHBOARD REPORT WIDGETS
+
+There are available report widgets to be used on OctoberCMS dashboard.
+
+#### Messages stats
+
+Shows basic messages statistics.
+
+
+#### New messages
+
+Shows number of new messages. The color changes to green if there are any.
+
+You can simply click widget to open Messages list.
+
+## Overriding form settings
+
+You can override some of the form settings in component dropdown (on page or layout) or by passing them in component call.
+
+
+#### Form settings
+
+*There is also an Alias column that contain component's alias of the used form and is saved in messages log (this field is invisible by default in messages table).*
+
+````
+[contactForm myForm]
+form_description = 'Form used in home page'
+disable_fields = 'name|message'
+send_btn_label = 'Go'
+form_success_msg = 'Ok, sent :)'
+form_error_msg = 'Houston, we have a problem'
+````
+
+
+You can override form's property in Twig component tag, eg:
+
+````
+{% component 'myForm' form_description = 'My other description' send_btn_label = 'Stay in touch' %}
+````
+
+This can be even more complex:
+````
+{% set myVar = 12345 %}
+{% component 'myForm' form_description = ('Current value: ' ~ myVar) %}
+````
+
+In email template you can access some of these variables like this:
+````
+Form alias: {{fields.form_alias}}
+Form description: {{fields.form_description}}
+````
+
+> When you override form description in ````{% component form_description = 'My description' %}````, description will be added as a **hidden field** into a form. Do not use this to store private data as this is easily visible in page HTML code!
+
+#### Override notification email options
+You can set different email address to which notification about sent form will be delivered and also change a notification template.
+
+*Template must exist in Settings > Mail > Mail configuration*.
+
+If you add a locale string to ````notification_template```` property (like ````notification_template_en````) than that one has priority and will be used if ````App::getLocale()```` returns ````en````.
+
+````
+[contactForm salesForm]
+disable_notifications = true
+notification_address_to = 'sales@domain.com'
+notification_address_from = 'contactform@domain.com'
+notification_template = 'notification-sales'
+notification_template_en = 'notification-sales-en'
+notification_template_cs = 'notification-sales-cs'
+notification_subject = 'Notification sent by form {{ fields.form_alias }} on {{ "now"|date }}'
+````
+
+> Local strings in `notification_template` canot be used in Twig!
+
+#### Override autoreply email options
+You can set different email address and name for autoreply message and also use different autoreply template.
+
+*Template must exist in Settings > Mail > Mail configuration*.
+
+If you add a locale string to ````autoreply_template```` or ````autoreply_address_from_name```` property (like ````autoreply_template_en```` or ````autoreply_address_from_name_en````) than that one has priority and will be used if ````App::getLocale()```` returns ````en````.
+
+````
+[contactForm orderForm]
+autoreply_address_from = 'order@domain.com'
+autoreply_address_from_name = 'Orders'
+autoreply_address_from_name_en = 'Orders'
+autoreply_address_from_name_cs = 'Objednávky'
+autoreply_template = 'autoreply-order'
+autoreply_template_en = 'autoreply-order-en'
+autoreply_template_cs = 'autoreply-order-cs'
+autoreply_subject = 'Autoreply sent by form {{ fields.form_alias }} on {{ "now"|date }}'
+````
+> Do you know that you can use form variables in an email template subject. In Settings > Mail templates create new template and set the Subject field to eg: `My form {{ fields.form_alias }}`.
+
+#### Disable some form fields
+You can disable some of defined form fields by passing their names in ````disable_fields```` component property.
+
+Several fields can be added while separated with pipe ````|````.
+
+````
+[contactForm]
+disable_fields = 'phone|name|confirmation'
+````
+
+Or you can disable some of functions:
+
+````
+[contactForm]
+disable_notifications = true
+disable_autoreply = true
+````
+
+----
+
+## HOWTO
+
+### Fight SPAM
+
+#### Prohibit sending URLs in a (message) field.
+
+* Use Custom rule
+* Add your validation error text
+* Use validation rule: `custom_not_regex`
+* Use validation: `/(http|https|ftp|ftps)\:\/\/?/`
+
+
+
+
+### Add an empty option to dropdown field
+
+You can easily add an empty option with empty ID and some value.
+
+
+
+#### Validate dropdown field
+
+If you want to validate dropdown options, you can use custom validation rule `in` with list of IDs as a validation pattern.
+
+
+
+
+----
+> My thanks goes to:
+> [OctoberCMS](http://www.octobercms.com) team members and supporters for this great system.
+> [Andrew Measham](https://unsplash.com/@andrewmeasham) for his photo.
+> [Font Awesome](http://fontawesome.io/icons/) for nice icons.
+
+
+Created by [Jan Vince](http://www.vince.cz), freelance web designer from Czech Republic.
diff --git a/plugins/janvince/smallcontactform/components/SmallContactForm.php b/plugins/janvince/smallcontactform/components/SmallContactForm.php
new file mode 100644
index 000000000..94dc95071
--- /dev/null
+++ b/plugins/janvince/smallcontactform/components/SmallContactForm.php
@@ -0,0 +1,1094 @@
+ 'janvince.smallcontactform::lang.controller.contact_form.name',
+ 'description' => 'janvince.smallcontactform::lang.controller.contact_form.description'
+ ];
+ }
+
+ public function defineProperties(){
+
+ return [
+
+ 'form_description' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.form_description',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.form_description_comment',
+ 'type' => 'string',
+ 'default' => null,
+ ],
+
+ 'disable_fields' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.disable_fields',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.disable_fields_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_form',
+ ],
+ 'send_btn_label' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.send_btn_label',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.send_btn_label_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_form',
+ ],
+ 'form_success_msg' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.form_success_msg',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.form_success_msg_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_form',
+ ],
+ 'form_error_msg' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.form_error_msg',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.form_error_msg_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_form',
+ ],
+
+
+ 'disable_notifications' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.disable_notifications',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.disable_notifications_comment',
+ 'type' => 'checkbox',
+ 'default' => false,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_notifications',
+ ],
+ 'notification_address_to' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.notification_address_to',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.notification_address_to_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_notifications',
+ ],
+ 'notification_address_from' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.notification_address_from',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.notification_address_from_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_notifications',
+ ],
+ 'notification_address_from_name' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.notification_address_from_name',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.notification_address_from_name_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_notifications',
+ ],
+ 'notification_template' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.notification_template',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.notification_template_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_notifications',
+ ],
+ 'notification_subject' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.notification_subject',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.notification_subject_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_notifications',
+ ],
+
+
+ 'disable_autoreply' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.disable_autoreply',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.disable_autoreply_comment',
+ 'type' => 'checkbox',
+ 'default' => false,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_autoreply',
+ ],
+ 'autoreply_address_from' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.autoreply_address_from',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.autoreply_address_from_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_autoreply',
+ ],
+ 'autoreply_address_from_name' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.autoreply_address_from_name',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.autoreply_address_from_name_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_autoreply',
+ ],
+ 'autoreply_address_replyto' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.autoreply_address_replyto',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.autoreply_address_replyto_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_autoreply',
+ ],
+ 'autoreply_template' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.autoreply_template',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.autoreply_template_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_autoreply',
+ ],
+ 'autoreply_subject' => [
+ 'title' => 'janvince.smallcontactform::lang.components.properties.autoreply_subject',
+ 'description' => 'janvince.smallcontactform::lang.components.properties.autoreply_subject_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_autoreply',
+ ],
+
+ 'allow_redirect' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.redirect.allow_redirect',
+ 'description' => 'janvince.smallcontactform::lang.settings.redirect.allow_redirect_comment',
+ 'type' => 'checkbox',
+ 'default' => false,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_redirect',
+ ],
+
+ 'redirect_url' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.redirect.redirect_url',
+ 'description' => 'janvince.smallcontactform::lang.settings.redirect.redirect_url_comment',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_redirect',
+ ],
+
+ 'redirect_url_external' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.redirect.redirect_url_external',
+ 'description' => 'janvince.smallcontactform::lang.settings.redirect.redirect_url_external_comment',
+ 'type' => 'checkbox',
+ 'default' => false,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_redirect',
+ ],
+
+ 'ga_success_event_allow' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.ga.ga_success_event_allow',
+ 'type' => 'checkbox',
+ 'default' => false,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_ga',
+ ],
+
+ 'ga_success_event_category' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.form_fields.event_category',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_ga',
+ ],
+
+ 'ga_success_event_action' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.form_fields.event_action',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_ga',
+ ],
+
+ 'ga_success_event_label' => [
+ 'title' => 'janvince.smallcontactform::lang.settings.form_fields.event_label',
+ 'type' => 'string',
+ 'default' => null,
+ 'group' => 'janvince.smallcontactform::lang.components.groups.override_ga',
+ ],
+
+ ];
+
+ }
+
+ public function onRun() {
+
+ $this->page['currentLocale'] = App::getLocale();
+
+ $this->page['formSentAlias'] = Session::get('formSentAlias', null);
+ $this->page['formError'] = Session::get('formError', null);
+ $this->page['formSuccess'] = Session::get('formSuccess', null);
+
+ $this->formDescription = $this->property('form_description');
+ $this->formRedirect = $this->property('redirect_url');
+
+ // Inject CSS assets if required
+ if(Settings::getTranslated('add_assets') && Settings::getTranslated('add_css_assets')){
+ $this->addCss('/modules/system/assets/css/framework.extras.css');
+ $this->addCss('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css');
+ }
+
+ // Inject JS assets if required
+ if(Settings::getTranslated('add_assets') && Settings::getTranslated('add_js_assets')){
+ $this->addJs('https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js');
+ $this->addJs('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js');
+ $this->addJs('/modules/system/assets/js/framework.js');
+ $this->addJs('/modules/system/assets/js/framework.extras.js');
+ }
+
+ }
+
+ public function onRender() {
+
+ // Component markup parameters are accesible only from onRender method!
+ if($this->formDescription != $this->property('form_description') ) {
+ $this->formDescriptionOverride = $this->property('form_description');
+ }
+
+ if($this->formRedirect != $this->property('redirect_url') ) {
+ $this->formRedirectOverride = $this->property('redirect_url');
+ }
+
+ }
+
+ /**
+ * Form handler
+ */
+ public function onFormSend(){
+
+ /**
+ * Validation
+ */
+ $this->setFieldsValidationRules();
+ $errors = [];
+
+ $this->post = Input::all();
+
+ // IP protection is enabled (has highest priority)
+ // But privacy must allow messages saving
+ if( Settings::getTranslated('add_ip_protection') and !Settings::getTranslated('privacy_disable_messages_saving') ) {
+
+ $max = ( Settings::getTranslated('add_ip_protection_count') ? intval(Settings::getTranslated('add_ip_protection_count')) : intval(e(trans('janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count_placeholder'))) );
+
+ if( empty($max) ) {
+ $max = 3;
+ }
+
+ $currentIp = Request::ip();
+
+ if( empty($currentIp) ) {
+ Log::error('SMALL CONTACT FORM ERROR: Could not get remote IP address!');
+ $errors[] = e(trans('janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_get_ip'));
+ } else {
+
+ $message = new Message;
+
+ if($message->testIPAddress($currentIp) >= $max) {
+ $errors[] = ( Settings::getTranslated('add_ip_protection_error_too_many_submits') ? Settings::getTranslated('add_ip_protection_error_too_many_submits') : e(trans('janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits_placeholder')) );
+ }
+
+ }
+
+ }
+
+ // Antispam validation if allowed
+ if( Settings::getTranslated('add_antispam')) {
+ $this->validationRules[('_protect-' . $this->alias)] = 'size:0';
+
+ if( !empty($this->post['_form_created']) ) {
+
+ try {
+ $delay = ( Settings::getTranslated('antispam_delay') ? intval(Settings::getTranslated('antispam_delay')) : intval(e(trans('janvince.smallcontactform::lang.settings.antispam.antispam_delay_placeholder'))) );
+
+ if(!$delay) {
+ $delay = 5;
+ }
+
+ $formCreatedTime = strtr(Input::get('_form_created'), 'jihgfedcba', '0123456789');
+
+ $this->post['_form_created'] = intval($formCreatedTime) + $delay;
+
+ $this->validationRules['_form_created'] = 'numeric|max:' . time();
+ }
+ catch (\Exception $e)
+ {
+ Log::error($e->getMessage());
+ $errors[] = e(trans('janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg_placeholder'));
+ }
+ }
+
+ }
+
+ // reCaptcha validation if enabled
+ if(Settings::getTranslated('add_google_recaptcha'))
+ {
+ try {
+ /**
+ * Text if allow_url_fopen is disabled
+ */
+ if (!ini_get('allow_url_fopen'))
+ {
+ $recaptcha = new ReCaptcha(Settings::get('google_recaptcha_secret_key'), new \ReCaptcha\RequestMethod\SocketPost());
+ }
+ else {
+ // allow_url_fopen = On
+ $recaptcha = new ReCaptcha(Settings::get('google_recaptcha_secret_key'));
+ }
+
+ $response = $recaptcha->setExpectedHostname($_SERVER['SERVER_NAME'])->verify(post('g-recaptcha-response'), $_SERVER['REMOTE_ADDR']);
+ }
+ catch(\Exception $e)
+ {
+ Log::error($e->getMessage());
+ $errors[] = e(trans('janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg_placeholder'));
+ }
+
+ if(!$response->isSuccess()) {
+ $errors[] = ( Settings::getTranslated('google_recaptcha_error_msg') ? Settings::getTranslated('google_recaptcha_error_msg') : e(trans('janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg_placeholder')));
+ }
+
+ }
+
+ // Validate sent data
+ $validator = Validator::make($this->post, $this->validationRules, $this->validationMessages);
+ $validator->valid();
+ $this->validationMessages = $validator->messages();
+ $this->setPostData($validator->messages());
+
+ if($validator->failed() or count($errors)){
+
+ // Form main error msg (can be overriden by component property)
+ if ( $this->property('form_error_msg') ) {
+
+ $errors[] = $this->property('form_error_msg');
+
+ } else {
+
+ $errors[] = ( Settings::getTranslated('form_error_msg') ? Settings::getTranslated('form_error_msg') : e(trans('janvince.smallcontactform::lang.settings.form.error_msg_placeholder')));
+
+ }
+
+ // Validation error msg for Antispam field
+ if( empty($this->postData[('_protect' . $this->alias)]['error']) && !empty($this->postData['_form_created']['error']) ) {
+ $errors[] = ( Settings::getTranslated('antispam_delay_error_msg') ? Settings::getTranslated('antispam_delay_error_msg') : e(trans('janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg_placeholder')));
+ }
+
+ Flash::error(implode(PHP_EOL, $errors));
+
+ if (Request::ajax()) {
+
+ $this->page['formSentAlias'] = $this->alias;
+ $this->page['formError'] = true;
+ $this->page['formSuccess'] = null;
+
+ } else {
+
+ Session::flash('formSentAlias', $this->alias);
+ Session::flash('formError', true);
+
+ }
+
+ // Fill hidden fields if request has errors to maintain
+ $this->formDescriptionOverride = post('_form_description');
+ $this->formRedirectOverride = post('_form_redirect');
+
+ } else {
+
+ // Form main success msg (can be overriden by component property)
+ if ($this->property('form_success_msg')) {
+
+ $successMsg = $this->property('form_success_msg');
+
+ } else {
+
+ $successMsg = ( Settings::getTranslated('form_success_msg') ? Settings::getTranslated('form_success_msg') : e(trans('janvince.smallcontactform::lang.settings.form.success_msg_placeholder')) );
+
+ }
+
+ $message = new Message;
+
+ // Store data in DB
+ $formDescription = !empty($this->post['_form_description']) ? e($this->post['_form_description']) : $this->property('form_description');
+ $messageObject = $message->storeFormData($this->postData, $this->alias, $formDescription);
+
+ // Send autoreply
+ $message->sendAutoreplyEmail($this->postData, $this->getProperties(), $this->alias, $formDescription, $messageObject);
+
+ // Send notification
+ $message->sendNotificationEmail($this->postData, $this->getProperties(), $this->alias, $formDescription, $messageObject);
+
+ /**
+ * Flash messages
+ */
+ Flash::success($successMsg);
+
+ if (Request::ajax()) {
+
+ $this->postData = [];
+ $this->page['formSentAlias'] = $this->alias;
+ $this->page['formSuccess'] = true;
+ $this->page['formError'] = null;
+
+ } else {
+
+ Session::flash('formSentAlias', $this->alias);
+ Session::flash('formSuccess', true);
+
+ }
+
+ /**
+ * Keep properties overrides after Ajax request (onRender method is not called)
+ */
+ if (Request::ajax()) {
+
+ $this->formDescriptionOverride = post('_form_description');
+ $this->formRedirectOverride = post('_form_redirect');
+
+ }
+
+ /**
+ * Redirects
+ *
+ * Redirect to defined page or to prevent repeated sending of form
+ * Clear data after success AJAX send
+ */
+ if( Settings::getTranslated('allow_redirect') or $this->property('allow_redirect') ) {
+
+ // Component markup parameter (eg. {{ component 'contactForm' redirect_url = '/form-success-'~page.id }} ) overrides component property
+ if(!empty($this->post['_form_redirect'])) {
+
+ $propertyRedirectUrl = e($this->post['_form_redirect']);
+
+ } else {
+
+ $propertyRedirectUrl = $this->property('redirect_url');
+
+ }
+
+ // If redirection is allowed but no URL provided, just refresh (if not AJAX)
+ if(empty($propertyRedirectUrl) and empty(Settings::getTranslated('redirect_url'))) {
+
+ Log::warning('SCF: Form redirect is allowed but no URL was provided!');
+
+ if (!Request::ajax()) {
+
+ return Redirect::refresh();
+
+ } else {
+
+ return;
+
+ }
+
+ }
+
+ // Overrides take precedence
+ if( !empty(Settings::getTranslated('redirect_url_external')) and !empty($this->property('redirect_url_external')) ) {
+
+ $path = $propertyRedirectUrl ? $propertyRedirectUrl : Settings::getTranslated('redirect_url');
+
+ } else {
+
+ $path = $propertyRedirectUrl ? url($propertyRedirectUrl) : url(Settings::getTranslated('redirect_url'));
+
+ }
+
+ return Redirect::to($path);
+
+ } else {
+
+ if (!Request::ajax()) {
+
+ return Redirect::refresh();
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Get plugin settings
+ * Twig access: contactForm.fields
+ * @return array
+ */
+ public function fields(){
+
+ $fields = Settings::getTranslated('form_fields', []);
+
+ if( !empty($this->property('disable_fields')) ) {
+
+ $disabledFields = explode( '|', $this->property('disable_fields') );
+
+ if(!is_array($disabledFields)) {
+ return $fields;
+ }
+
+ foreach ($fields as $key => $value) {
+
+ if( isset($value['name']) and in_array(trim($value['name']), $disabledFields) ) {
+ unset($fields[$key]);
+ }
+
+ }
+
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Get form attributes
+ */
+ public function getFormAttributes(){
+
+ $attributes = [];
+
+ $attributes['request'] = $this->alias . '::onFormSend';
+ $attributes['files'] = true;
+
+ // Disabled hard coded hash URL in 1.41.0 as dynamic redirect is now available
+ // $attributes['url'] = '#scf-' . $this->alias;
+
+ $attributes['method'] = 'POST';
+ $attributes['class'] = null;
+ $attributes['id'] = 'scf-form-id-' . $this->alias;
+
+ if( Settings::getTranslated('form_allow_ajax', 0) ) {
+
+ $attributes['data-request'] = $this->alias . '::onFormSend';
+ $attributes['data-request-validate'] = 'data-request-validate';
+ $attributes['data-request-files'] = 'data-request-files';
+ $attributes['data-request-update'] = "'". $this->alias ."::scf-message':'#scf-message-". $this->alias ."','". $this->alias ."::scf-form':'#scf-form-". $this->alias ."'";
+
+ }
+
+ if( Settings::getTranslated('form_css_class') ) {
+ $attributes['class'] .= Settings::getTranslated('form_css_class');
+ }
+
+ if( !empty(Input::all()) ) {
+ $attributes['class'] .= ' was-validated';
+ }
+
+ if( Settings::getTranslated('form_send_confirm_msg') and Settings::getTranslated('form_allow_confirm_msg') ) {
+
+ $attributes['data-request-confirm'] = Settings::getTranslated('form_send_confirm_msg');
+
+ }
+
+ // Disable browser validation if enabled
+ if(!empty(Settings::getTranslated('form_disable_browser_validation'))){
+ $attributes['novalidate'] = "novalidate";
+ }
+
+ return $attributes;
+
+ }
+
+ /**
+ * Generate field HTML code
+ * @return string
+ */
+ public function getFieldHtmlCode(array $fieldSettings){
+
+ if(empty($fieldSettings['name']) && empty($fieldSettings['type'])){
+ return NULL;
+ }
+
+ $fieldType = Settings::getFieldTypes($fieldSettings['type']);
+ $fieldRequired = $this->isFieldRequired($fieldSettings);
+
+ // If there is a custom code, return it only
+ if( !empty($fieldSettings['type']) and $fieldSettings['type'] == 'custom_code' and !empty($fieldSettings['field_custom_code']) ) {
+
+ if( !empty($fieldSettings['field_custom_code_twig']) ) {
+ return(Twig::parse($fieldSettings['field_custom_code']));
+ } else {
+ return($fieldSettings['field_custom_code']);
+ }
+ }
+
+ $output = [];
+
+ $wrapperCss = ( $fieldSettings['wrapper_css'] ? $fieldSettings['wrapper_css'] : $fieldType['wrapper_class'] );
+
+ // Add wrapper error class if there are any
+ if(!empty($this->postData[$fieldSettings['name']]['error'])){
+ $wrapperCss .= ' has-error';
+ }
+
+ $output[] = '';
+
+ // Checkbox wrapper
+ if ($fieldSettings['type'] == 'checkbox') {
+ $output[] = '
';
+ }
+
+ // Label classic
+ if( !empty($fieldSettings['label']) and !empty($fieldType['label']) ){
+ $output[] = '' . Settings::getDictionaryTranslated($fieldSettings['label']) . ' ';
+ }
+
+ // Label as container
+ if( !empty($fieldSettings['label']) and empty($fieldType['label']) ){
+ $output[] = '';
+ }
+
+
+ // Add help-block if there are errors
+ if(!empty($this->postData[$fieldSettings['name']]['error'])){
+ $output[] = '' . Settings::getDictionaryTranslated($this->postData[$fieldSettings['name']]['error']) . " ";
+ }
+
+ // Field attributes
+ $attributes = [
+ 'id' => $this->alias . '-' . $fieldSettings['name'],
+ 'class' => null
+ ];
+
+ $tagClass = $fieldSettings['field_css'] ? $fieldSettings['field_css'] : $fieldType['field_class'];
+
+ if(!empty($tagClass)) {
+ $attributes['class'] = $tagClass;
+ }
+
+ if(!empty($fieldType['use_name_attribute'])) {
+ $attributes['name'] = $fieldSettings['name'];
+ }
+
+ if ( !empty($this->postData[$fieldSettings['name']]['value']) && empty($fieldType['html_close']) ) {
+
+ if ($fieldSettings['type'] == 'checkbox') {
+ $attributes['checked'] = null;
+ } else {
+ $attributes['value'] = $this->postData[$fieldSettings['name']]['value'];
+ }
+ }
+
+ // Placeholders if enabled
+ if(Settings::getTranslated('form_use_placeholders') and $fieldSettings['type'] <> 'checkbox'){
+ $attributes['placeholder'] = Settings::getDictionaryTranslated($fieldSettings['label']);
+ }
+
+ // Autofocus only when no error
+ if(!empty($fieldSettings['autofocus']) && !Flash::error()){
+ $attributes['autofocus'] = NULL;
+ }
+
+ // Add custom attributes from field settings
+ if(!empty($fieldType['attributes'])){
+ $attributes = array_merge($attributes, $fieldType['attributes']);
+ }
+
+ // Add error class if there are any and autofocus field
+ if(!empty($this->postData[$fieldSettings['name']]['error'])){
+ $attributes['class'] = $attributes['class'] . ' error is-invalid';
+
+ if(empty($this->errorAutofocus)){
+ $attributes['autofocus'] = NULL;
+ $this->errorAutofocus = true;
+ }
+
+ }
+
+ if($fieldRequired){
+ $attributes['required'] = NULL;
+ }
+
+
+ $output[] = '<' . $fieldType['html_open'] . ' ' . $this->formatAttributes($attributes) . '>';
+
+ // For dropdown add options
+ if( $fieldSettings['type'] == 'dropdown' && count($fieldSettings['field_values']) ) {
+
+ $valuesCounter = 1;
+
+ foreach($fieldSettings['field_values'] as $fieldValue) {
+
+ if( !empty($this->postData[$fieldSettings['name']]['value']) && $this->postData[$fieldSettings['name']]['value'] == $fieldValue['field_value_id'] ){
+ $optionAttribute = 'selected';
+ } else {
+ $optionAttribute = null;
+ }
+
+ $output[] = "" . $fieldValue['field_value_content'] . " ";
+
+ $valuesCounter++;
+
+ }
+
+ }
+ // For pair tags insert value between
+ if(!empty($this->postData[$fieldSettings['name']]['value']) && !empty($fieldType['html_close'])){
+ $output[] = $this->postData[$fieldSettings['name']]['value'];
+ }
+
+ // For tags without label put text inline
+ if( empty( $fieldType['label'] ) ){
+ $output[] = Settings::getDictionaryTranslated($fieldSettings['label']);
+ }
+
+ // If there is a custom content
+ if (!empty($fieldSettings['type']) and $fieldSettings['type'] == 'custom_content' and !empty($fieldSettings['field_custom_content'])) {
+ $output[] = $fieldSettings['field_custom_content'];
+ }
+
+ if(!empty($fieldType['html_close'])){
+ $output[] = '' . $fieldType['html_close'] . '>';
+ }
+
+ // Label as container
+ if( !empty($fieldSettings['label']) and empty($fieldType['label']) ){
+ $output[] = ' ';
+ }
+
+ // Checkbox wrapper
+ if ($fieldSettings['type'] == 'checkbox') {
+ $output[] = '
';
+ }
+
+ $output[] = "
";
+
+ return(implode('', $output));
+
+ }
+
+ /**
+ * Generate antispam field HTML code
+ * @return string
+ */
+ public function getAntispamFieldHtmlCode(){
+
+ if( !Settings::getTranslated('add_antispam') ){
+ return NULL;
+ }
+
+ $output = [];
+
+ $output[] = '';
+
+ $output[] = '' . ( Settings::getTranslated('antispam_label') ? Settings::getTranslated('antispam_label') : e(trans('janvince.smallcontactform::lang.settings.antispam.antispam_label_placeholder')) ) . ' ';
+
+ $output[] = ' ';
+
+ // Add help-block if there are errors
+ if(!empty($this->postData[('_protect'.$this->alias)]['error'])){
+ $output[] = '' . ( Settings::getTranslated('antispam_error_msg') ? Settings::getTranslated('antispam_error_msg') : e(trans('janvince.smallcontactform::lang.settings.antispam.antispam_error_msg_placeholder')) ) . " ";
+ }
+
+ // Field attributes
+ $attributes = [
+ 'id' => '_protect-'.$this->alias,
+ 'name' => '_protect',
+ 'class' => '_protect form-control',
+ 'value' => 'http://',
+ ];
+
+ // Add error class if field is not empty
+ if( Input::get('_protect-'.$this->alias) ){
+ $attributes['class'] = $attributes['class'] . ' error';
+
+ if(empty($this->errorAutofocus)){
+ $attributes['autofocus'] = NULL;
+ $this->errorAutofocus = true;
+ }
+
+ }
+
+ $output[] = ' formatAttributes($attributes) . '>';
+
+ $output[] = "
";
+
+ $output[] = "
+
+ ";
+
+ return(implode('', $output));
+
+ }
+
+
+ /**
+ * Generate description field HTML code
+ * @return string
+ */
+ public function getDescriptionFieldHtmlCode(){
+
+ if( !$this->formDescriptionOverride ){
+ return NULL;
+ }
+
+ $output = [];
+
+ // Field attributes
+ $attributes = [
+ 'id' => '_form_description-'.$this->alias,
+ 'type' => 'hidden',
+ 'name' => '_form_description',
+ 'class' => '_form_description form-control',
+ 'value' => $this->formDescriptionOverride,
+ ];
+
+ $output[] = ' formatAttributes($attributes) . '>';
+
+ return(implode('', $output));
+
+ }
+
+ /**
+ * Generate redirect field HTML code
+ * @return string
+ */
+ public function getRedirectFieldHtmlCode(){
+
+ if (empty(Settings::getTranslated('allow_redirect')) and empty($this->property('allow_redirect'))) {
+ return NULL;
+ }
+
+ if( !$this->formRedirectOverride ){
+ return NULL;
+ }
+
+ $output = [];
+
+ // Field attributes
+ $attributes = [
+ 'id' => '_form_redirect-'.$this->alias,
+ 'type' => 'hidden',
+ 'name' => '_form_redirect',
+ 'class' => '_form_redirect form-control',
+ 'value' => $this->formRedirectOverride,
+ ];
+
+ $output[] = ' formatAttributes($attributes) . '>';
+
+ return(implode('', $output));
+
+ }
+
+ /**
+ * Generate success GA event field HTML code
+ * @return string
+ */
+ public function getGaSuccessEventHtmlCode($addScriptTag = false)
+ {
+
+ // If GA success event is not allowed
+ if (empty(Settings::getTranslated('ga_success_event_allow')) and empty($this->property('ga_success_event_allow'))) {
+ return;
+ }
+
+ $output = [];
+
+ // Field attributes
+ $attributes = [
+ 'hitType' => 'event',
+ 'eventCategory' => ($this->property('ga_success_event_category') ? e($this->property('ga_success_event_category')) : Settings::getTranslated('ga_success_event_category')),
+ 'eventAction' => ($this->property('ga_success_event_action') ? e($this->property('ga_success_event_action')) : Settings::getTranslated('ga_success_event_action')),
+ 'eventLabel' => ($this->property('ga_success_event_label') ? e($this->property('ga_success_event_label')) : Settings::getTranslated('ga_success_event_label')),
+ ];
+
+ if($addScriptTag) {
+ $output[] = "";
+ }
+
+ return (implode('', $output));
+ }
+
+ /**
+ * Generate submit button field HTML code
+ * @return string
+ */
+ public function getSubmitButtonHtmlCode(){
+
+ if( !count($this->fields()) ){
+ return e(trans('janvince.smallcontactform::lang.controller.contact_form.no_fields'));
+ }
+
+ $output = [];
+
+ $wrapperCss = ( Settings::getTranslated('send_btn_wrapper_css') ? Settings::getTranslated('send_btn_wrapper_css') : e(trans('janvince.smallcontactform::lang.settings.buttons.send_btn_wrapper_css_placeholder')) );
+
+ $output[] = '';
+
+ $output[] = '';
+
+ if ($this->property('send_btn_label')) {
+
+ $output[] = $this->property('send_btn_label');
+
+ } else {
+
+ $output[] = ( Settings::getTranslated('send_btn_text') ? Settings::getTranslated('send_btn_text') : e(trans('janvince.smallcontactform::lang.settings.buttons.send_btn_text_placeholder')) );
+
+ }
+
+ $output[] = ' ';
+
+ $output[] = "
";
+
+ return(implode('', $output));
+
+ }
+
+ /**
+ * Get reCaptcha wrapper class
+ * @return string
+ */
+ public function getReCaptchaWrapperClass(){
+
+ $wrapperCss = ( Settings::getTranslated('google_recaptcha_wrapper_css') ? Settings::getTranslated('google_recaptcha_wrapper_css') : e(trans('janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css_placeholder')) );
+
+ return $wrapperCss;
+
+ }
+
+ /**
+ * Generate validation rules and messages
+ */
+ private function setFieldsValidationRules(){
+
+ $fieldsDefinition = $this->fields();
+
+ $validationRules = [];
+ $validationMessages = [];
+ foreach($fieldsDefinition as $field){
+
+ if(!empty($field['validation'])) {
+ $rules = [];
+
+ foreach($field['validation'] as $rule) {
+
+ if( $rule['validation_type']=='custom' && !empty($rule['validation_custom_type']) ){
+
+ if(!empty($rule['validation_custom_pattern'])) {
+
+ switch ($rule['validation_custom_type']) {
+
+ /**
+ * Keep regex pattern in an array
+ */
+ case "regex":
+
+ $rules[] = [$rule['validation_custom_type'], $rule['validation_custom_pattern']];
+
+ break;
+
+ default:
+
+ $rules[] = $rule['validation_custom_type'] . ':' . $rule['validation_custom_pattern'];
+
+ break;
+
+ }
+
+
+
+ } else {
+
+ $rules[] = $rule['validation_custom_type'];
+
+ }
+
+ if(!empty($rule['validation_error'])){
+
+ $validationMessages[($field['name'] . '.' . $rule['validation_custom_type'] )] = Settings::getDictionaryTranslated($rule['validation_error']);
+ }
+
+ } else {
+
+ $rules[] = $rule['validation_type'];
+
+ if(!empty($rule['validation_error'])){
+
+ $validationMessages[($field['name'] . '.' . $rule['validation_type'] )] = Settings::getDictionaryTranslated($rule['validation_error']);
+ }
+ }
+ }
+
+ $validationRules[$field['name']] = $rules;
+ }
+ }
+
+ $this->validationRules = $validationRules;
+ $this->validationMessages = $validationMessages;
+
+ }
+
+
+ /**
+ * Generate post data with errors
+ */
+ private function setPostData(MessageBag $validatorMessages){
+
+ foreach( $this->fields() as $field){
+
+ $this->postData[ $field['name'] ] = [
+ 'value' => e(Input::get($field['name'])),
+ 'error' => $validatorMessages->first($field['name']),
+ ];
+
+ }
+
+ }
+
+ /**
+ * Format attributes array
+ * @return array
+ */
+ private function formatAttributes(array $attributes, $jsArray = false) {
+
+ $output = [];
+
+ foreach ($attributes as $key => $value) {
+ $output[] = $key . ($jsArray ? ': "' : '="') . $value . '"';
+ }
+
+ return implode(($jsArray ? ', ' : ' '), $output);
+ }
+
+ /**
+ * Search for required validation type
+ */
+ private function isFieldRequired($fieldSettings){
+
+ if(empty($fieldSettings['validation'])){
+ return false;
+ }
+
+ foreach($fieldSettings['validation'] as $rule) {
+ if(!empty($rule['validation_type']) && $rule['validation_type'] == 'required'){
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/janvince/smallcontactform/components/smallcontactform/default.htm b/plugins/janvince/smallcontactform/components/smallcontactform/default.htm
new file mode 100644
index 000000000..dbad0b63c
--- /dev/null
+++ b/plugins/janvince/smallcontactform/components/smallcontactform/default.htm
@@ -0,0 +1,11 @@
+
+
+
+ {% partial __SELF__ ~ '::scf-message' %}
+
+
+
+ {% partial __SELF__ ~ '::scf-form' %}
+
+
+
\ No newline at end of file
diff --git a/plugins/janvince/smallcontactform/components/smallcontactform/scf-form.htm b/plugins/janvince/smallcontactform/components/smallcontactform/scf-form.htm
new file mode 100644
index 000000000..cfcc3aa13
--- /dev/null
+++ b/plugins/janvince/smallcontactform/components/smallcontactform/scf-form.htm
@@ -0,0 +1,61 @@
+{% if formSentAlias == __SELF__.alias and formError is empty and settingsGet('form_hide_after_success', 0) %}
+
+ {# no errors and set to hide after send #}
+
+{% else %}
+
+ {{ form_open(__SELF__.getFormAttributes) }}
+
+ {% for field in __SELF__.fields %}
+
+ {{ __SELF__.getFieldHtmlCode(field)|raw }}
+
+ {% endfor %}
+
+ {{ __SELF__.getAntispamFieldHtmlCode({})|raw }}
+
+ {{ __SELF__.getDescriptionFieldHtmlCode({})|raw }}
+
+ {{ __SELF__.getRedirectFieldHtmlCode({})|raw }}
+
+ {% if (settingsGet('google_recaptcha_version') is null or settingsGet('google_recaptcha_version') == 'v2checkbox') and settingsGet('add_google_recaptcha') and settingsGet('google_recaptcha_site_key') %}
+
+
+
+ {% endif %}
+
+ {{ __SELF__.getSubmitButtonHtmlCode({})|raw }}
+
+ {{ form_close() }}
+
+ {% if settingsGet('add_google_recaptcha') and settingsGet('google_recaptcha_scripts_allow') %}
+
+
+
+ {% if settingsGet('google_recaptcha_version') == 'v2invisible' %}
+
+
+
+ {% endif %}
+
+ {% endif %}
+
+{% endif %}
diff --git a/plugins/janvince/smallcontactform/components/smallcontactform/scf-message.htm b/plugins/janvince/smallcontactform/components/smallcontactform/scf-message.htm
new file mode 100644
index 000000000..20e14184b
--- /dev/null
+++ b/plugins/janvince/smallcontactform/components/smallcontactform/scf-message.htm
@@ -0,0 +1,28 @@
+
+{% if formSentAlias == __SELF__.alias %}
+
+ {% if formSuccess %}
+
+ {{ __SELF__.getGaSuccessEventHtmlCode(true)|raw }}
+
+ {% flash success %}
+
+
+ {{ html_entity_decode(message)|nl2br }}
+
+
+ {% endflash %}
+
+ {% elseif formError %}
+
+ {% flash error %}
+
+
+ {{ html_entity_decode(message)|nl2br }}
+
+
+ {% endflash %}
+
+ {% endif %}
+
+{% endif %}
\ No newline at end of file
diff --git a/plugins/janvince/smallcontactform/composer.json b/plugins/janvince/smallcontactform/composer.json
new file mode 100644
index 000000000..b83feb732
--- /dev/null
+++ b/plugins/janvince/smallcontactform/composer.json
@@ -0,0 +1,8 @@
+{
+ "name": "janvince/smallcontactform-plugin",
+ "type": "october-plugin",
+ "description": "None",
+ "require": {
+ "composer/installers": "~1.0"
+ }
+}
\ No newline at end of file
diff --git a/plugins/janvince/smallcontactform/controllers/Messages.php b/plugins/janvince/smallcontactform/controllers/Messages.php
new file mode 100644
index 000000000..293dc4c5d
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/Messages.php
@@ -0,0 +1,158 @@
+count();
+ break;
+
+ case 'new_count':
+ return Message::isNew()->count();
+ break;
+
+ case 'latest_message_date':
+
+ $data = Message::orderBy( 'created_at', 'DESC' )->first();
+
+ if ( !empty( $data->created_at ) ) {
+ Carbon::setLocale( App::getLocale() );
+ return Carbon::createFromFormat( 'Y-m-d H:i:s', $data->created_at )->diffForHumans();
+ }
+
+ return NULL;
+ break;
+
+ case 'latest_message_name':
+ $data = Message::orderBy( 'created_at', 'DESC' )->first();
+
+ if( !empty( $data->name ) ) {
+ return $data->name;
+ }
+
+ return NULL;
+ break;
+
+ default:
+ return NULL;
+ break;
+
+ }
+
+ }
+
+ /**
+ * Preview page view
+ * @param $id
+ */
+ public function preview( $id ){
+
+ $message = Message::find( $id );
+
+ if ( $message ) {
+
+ $this->vars['message'] = $message;
+ $message->new_message = 0;
+ $message->save();
+
+ } else{
+
+ Flash::error( e( trans( 'janvince.smallcontactform::lang.controller.preview.record_not_found') ) );
+ return Redirect::to( Backend::url( 'janvince/smallcontactform/messages' ) );
+
+ }
+
+ }
+
+ /**
+ * Index page view
+ */
+ public function index(){
+
+ parent::index();
+
+ if (!$this->user->hasAccess('janvince.smallcontactform.access_messages')) {
+
+ Flash::error( e(trans('janvince.smallcontactform::lang.controllers.index.unauthorized')) );
+ return Redirect::to( Backend::url('/') );
+
+ }
+
+ }
+
+ /**
+ * Mark messages as read
+ * @param $record
+ */
+ public function onMarkRead(){
+
+ if (!$this->user->hasAccess('janvince.smallcontactform.access_messages')) {
+
+ Flash::error( e(trans('janvince.smallcontactform::lang.controllers.index.unauthorized')) );
+ return;
+
+ }
+
+ if ( ($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds) ) {
+
+ foreach ($checkedIds as $item) {
+ if (!$record = Message::find($item)) {
+ continue;
+ }
+
+ $record->new_message = 0;
+ $record->save();
+
+ }
+
+ Flash::success( e(trans('janvince.smallcontactform::lang.controller.scoreboard.mark_read_success')) );
+
+ return $this->listRefresh();
+
+ }
+
+ }
+
+}
diff --git a/plugins/janvince/smallcontactform/controllers/messages/_list_toolbar.htm b/plugins/janvince/smallcontactform/controllers/messages/_list_toolbar.htm
new file mode 100644
index 000000000..5595c3ae2
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/_list_toolbar.htm
@@ -0,0 +1,72 @@
+
+
+
+
+ = e(trans('janvince.smallcontactform::lang.controller.scoreboard.new_count')) ?> = $this->getRecordsStats('new_count'); ?>
+ = e(trans('janvince.smallcontactform::lang.controller.scoreboard.read_count')) ?> = $this->getRecordsStats('read_count'); ?>
+
+
+
+
+
= e(trans('janvince.smallcontactform::lang.controller.scoreboard.new_count')) ?>
+
= $this->getRecordsStats('new_count'); ?>
+
= e(trans('janvince.smallcontactform::lang.controller.scoreboard.new_description')) ?>
+
+
+
+
+
= e(trans('janvince.smallcontactform::lang.controller.scoreboard.latest_record')) ?>
+
= $this->getRecordsStats('latest_message_name'); ?>
+
= $this->getRecordsStats('latest_message_date'); ?>
+
+
+
+
+
+
+
diff --git a/plugins/janvince/smallcontactform/controllers/messages/config_export.yaml b/plugins/janvince/smallcontactform/controllers/messages/config_export.yaml
new file mode 100644
index 000000000..4d8cfbf3c
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/config_export.yaml
@@ -0,0 +1,10 @@
+# ===================================
+# Import/Export Behavior Config
+# ===================================
+
+export:
+ title: janvince.smallcontactform::lang.controllers.messages.export
+ modelClass: JanVince\SmallContactForm\Models\MessageExport
+ list: $/janvince/smallcontactform/models/message/columns_export.yaml
+ redirect: /janvince/smallcontactform/messages
+ fileName: scf-export.csv
diff --git a/plugins/janvince/smallcontactform/controllers/messages/config_filter.yaml b/plugins/janvince/smallcontactform/controllers/messages/config_filter.yaml
new file mode 100644
index 000000000..206309bd7
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/config_filter.yaml
@@ -0,0 +1,10 @@
+# ===================================
+# Filter Scope Definitions
+# ===================================
+
+scopes:
+
+ date:
+ label: janvince.smallcontactform::lang.controller.filter.columns.date
+ type: daterange
+ conditions: date >= ':after' AND date <= ':before'
diff --git a/plugins/janvince/smallcontactform/controllers/messages/config_list.yaml b/plugins/janvince/smallcontactform/controllers/messages/config_list.yaml
new file mode 100644
index 000000000..17f3a102c
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/config_list.yaml
@@ -0,0 +1,43 @@
+# ===================================
+# List Behavior Config
+# ===================================
+
+# Model List Column configuration
+list: $/janvince/smallcontactform/models/message/columns.yaml
+
+# Model Class name
+modelClass: JanVince\SmallContactForm\Models\Message
+
+# List Title
+title: "janvince.smallcontactform::lang.controllers.messages.list_title"
+
+# Message to display if the list is empty
+noRecordsMessage: backend::lang.list.no_records
+
+recordUrl: 'janvince/smallcontactform/messages/preview/:id'
+
+# Records to display per page
+recordsPerPage: 20
+
+# Displays the list column set up button
+showSetup: true
+
+# Displays the sorting link on each column
+showSorting: true
+
+# Default sorting column
+defaultSort:
+ column: created_at
+ direction: desc
+
+# Display checkboxes next to each record
+showCheckboxes: true
+
+# Toolbar widget configuration
+toolbar:
+ # Partial for toolbar buttons
+ buttons: list_toolbar
+
+ # Search widget configuration
+ search:
+ prompt: backend::lang.list.search_prompt
diff --git a/plugins/janvince/smallcontactform/controllers/messages/export.htm b/plugins/janvince/smallcontactform/controllers/messages/export.htm
new file mode 100644
index 000000000..c0c73df08
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/export.htm
@@ -0,0 +1,25 @@
+
+
+
+
+= Form::open(['class' => 'layout']) ?>
+
+
+ = $this->exportRender() ?>
+
+
+
+
+ = e(trans('janvince.smallcontactform::lang.controllers.messages.export')) ?>
+
+
+
+= Form::close() ?>
diff --git a/plugins/janvince/smallcontactform/controllers/messages/index.htm b/plugins/janvince/smallcontactform/controllers/messages/index.htm
new file mode 100644
index 000000000..766877d92
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/index.htm
@@ -0,0 +1,2 @@
+
+= $this->listRender() ?>
diff --git a/plugins/janvince/smallcontactform/controllers/messages/preview.htm b/plugins/janvince/smallcontactform/controllers/messages/preview.htm
new file mode 100644
index 000000000..59a1ee86a
--- /dev/null
+++ b/plugins/janvince/smallcontactform/controllers/messages/preview.htm
@@ -0,0 +1,71 @@
+
+
+
+
+fatalError): ?>
+
+
+
+
+
+
created_at->format('j.n.Y H:i:s')); ?>
+
+
+
+
+
+
+
+ form_data as $key => $field) : ?>
+
+
+
+
+
+
+
+
+ uploads): ?>
+
+
+ Uploads
+
+ uploads as $upload) : ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ remote_ip)) { echo($message->remote_ip); } ?>
+
+
+
+ form_description)) { echo($message->form_description); } ?>
+
+
+
+
+
+
+
+
+
+
+ = e($this->fatalError) ?>
+
+
+
+
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
+
diff --git a/plugins/janvince/smallcontactform/lang/cs/lang.php b/plugins/janvince/smallcontactform/lang/cs/lang.php
new file mode 100644
index 000000000..a97e67c19
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/cs/lang.php
@@ -0,0 +1,523 @@
+ [
+ 'name' => 'Kontaktní formulář',
+ 'description' => 'Jednoduchý kontaktní formulář',
+ 'category' => 'Small plugins',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Přístup k seznamu zpráv',
+ 'access_settings' => 'Přístup k nastavení',
+ 'delete_messages' => 'Smazat vybrané zprávy',
+ 'export_messages' => 'Exportovat zprávy',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Kontaktní formulář',
+ 'messages' => 'Zprávy',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Kontaktní formulář',
+ 'description' => 'Přidá do stránky kontaktní formulář',
+ 'no_fields' => 'Přidejte prosím nějaká formulářová pole v administraci systému (Nastavení > Kontaktní formulář > Pole)...',
+ ],
+
+ 'filter' => [
+ 'date' => 'Rozmezí data',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Zprávy',
+ 'latest_record' => 'nejnovější od',
+ 'new_count' => 'Nové',
+ 'new_description' => 'Zpráv',
+ 'read_count' => 'Přečtené',
+ 'all_count' => 'Celkem',
+ 'all_description' => 'Zpráv',
+ 'settings_btn' => 'Nastavení formuláře',
+ 'mark_read' => 'Označit jako přečtené',
+ 'mark_read_confirm' => 'Opravdu chcete vybrané zprávy označit jako přečtené?',
+ 'mark_read_success' => 'Zprávy byly označeny jako přečtené.',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Zpráva nebyla nalezena!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Datum a čas',
+ 'form_data' => 'Data formuláře',
+ 'name' => 'Jméno',
+ 'email' => 'Email',
+ 'message' => 'Zpráva',
+ 'new_message' => 'Stav',
+ 'new' => 'Nová',
+ 'read' => 'Přečtená',
+ 'remote_ip' => 'IP odesílatele',
+ 'created_at' => 'Datum vytvoření',
+ 'updated_at' => 'Datum aktualizace',
+ ]
+
+ ],
+
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+
+ 'list_title' => 'Zprávy',
+ 'preview' => 'Náhled',
+ 'preview_title' => 'Zpráva z kontaktního formuláře',
+ 'preview_date' => 'Ze dne:',
+ 'preview_content_title' => 'Obsah:',
+ 'remote_ip' => 'odesláno z ip',
+ 'form_alias' => 'Alias',
+ 'form_description' => 'Popisek',
+ 'export' => 'Export',
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Neoprávněný přístup!',
+ ],
+
+ ],
+
+ 'mail' => [
+
+ 'templates' => [
+ 'autoreply' => 'Zpráva automatické odpovědi z kontaktního formuláře (Anglicky)',
+ 'notification' => 'Notifikace z kontaktního formuláře (Anglicky)',
+ 'autoreply_cs' => 'Zpráva automatické odpovědi z kontaktního formuláře (Česky)',
+ 'notification_cs' => 'Notifikace z kontaktního formuláře (Česky)',
+ ]
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+
+ 'messages' => [
+ 'label' => 'Kontaktní formulář - Přehled zpráv',
+ 'title' => 'Přehled zpráv',
+ 'messages_all' => 'Vše',
+ 'messages_new' => 'Nové',
+ 'messages_read' => 'Přečtené',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Kontaktní formulář - Nové zprávy',
+ 'title' => 'Nové zprávy',
+ 'link_text' => 'Klikněte pro zobrazení přehledu zpráv',
+ ],
+
+ ],
+
+ ],
+
+ 'settings' => [
+
+ 'form' => [
+
+ 'css_class' => 'CSS třída formuláře',
+
+ 'use_placeholders' => 'Používat zástupný text (placeholder)',
+ 'use_placeholders_comment' => 'Místo popisků nad formulářovými poli bude použitý zástupný text',
+
+ 'disable_browser_validation' => 'Zakázat validaci prohlížečem',
+ 'disable_browser_validation_comment' => 'Nepovolit prohlížeči použít vlastní validaci a zobrazovat výstrahy.',
+
+ 'success_msg' => 'Zpráva po úspěšném odeslání',
+ 'success_msg_placeholder' => 'Formulář byl v pořádku odeslán.',
+
+ 'error_msg' => 'Chybová zpráva',
+ 'error_msg_placeholder' => 'Při odesílání formuláře došlo k chybě!',
+
+ 'allow_ajax' => 'Povolit AJAX',
+ 'allow_ajax_comment' => 'Povolí AJAX, ale umožní fungování formuláře i na prohlížečích s vypnutým JavaScriptem',
+
+ 'allow_confirm_msg' => 'Požadovat potvrzení před odesláním',
+ 'allow_confirm_msg_comment' => 'Zobrazí potvrzovací okno před odesláním formuláře',
+
+ 'send_confirm_msg' => 'Text potvrzení',
+ 'send_confirm_msg_placeholder' => 'Opravdu chcete odeslat formulář?',
+
+ 'hide_after_success' => 'Skrýt formulář po úspěšném odeslání',
+ 'hide_after_success_comment' => 'Po odeslání zobrazí pouze zprávu z potvrzením bez formuláře',
+
+ 'add_assets' => 'Přidat doplňky',
+ 'add_assets_comment' => 'Automaticky vloží potřebné CSS styly a JS skripty (Více informací je v souboru README.md)',
+
+ 'add_css_assets' => 'Přidat CSS styly',
+ 'add_css_assets_comment' => 'Vloží všechny potřebné styly',
+
+ 'add_js_assets' => 'Přidat JS skripty',
+ 'add_js_assets_comment' => 'Vloží všechny potřebné skripty',
+
+ 'form_ga_event_success' => 'Událost po úspěšném odeslání',
+
+ ],
+
+ 'sections' => [
+ 'ga_events' => 'Události'
+ ],
+
+ 'ga' => [
+ 'ga_success_event_allow' => 'Zaznamenat událost po úspěšném odeslání formuláře',
+
+ ],
+
+ 'buttons' => [
+ 'send_btn_text' => 'Text odesílacího tlačítka',
+ 'send_btn_text_placeholder' => 'Odeslat',
+
+ 'send_btn_css_class' => 'CSS třída odesílacího tlačítka',
+ 'send_btn_css_class_placeholder' => 'btn btn-primary',
+
+ 'send_btn_wrapper_css' => 'CSS třída kontejneru',
+ 'send_btn_wrapper_css_placeholder' => 'form-group',
+
+ ],
+
+ 'redirect' => [
+
+ 'allow_redirect' => 'Přesměrovat po úspěšném odeslání',
+ 'allow_redirect_comment' => 'Přesměrovat na jinou stránku po úspěšném odeslání formuláře',
+
+ 'redirect_url' => 'URL stránky pro přesměrování',
+ 'redirect_url_comment' => 'Vložte URL adresu stránky, kam bude přesměrováno (např. /kontakt/diky)',
+ 'redirect_url_placeholder' => '/kontakt/diky',
+
+ 'redirect_url_external' => 'Externí URL',
+ 'redirect_url_external_comment' => 'Toto je adresa externí stránky (např. http://www.domain.com)',
+
+ ],
+
+ 'form_fields' => [
+ 'prompt' => 'Přidat nové pole formuláře',
+
+ 'name' => 'NÁZEV POLE',
+ 'name_comment' => 'Malými písmeny bez diakritiky (např. jmeno, email, vase_poznamka, ...)',
+
+ 'type' => 'Typ pole',
+
+ 'label' => 'Popisek (label)',
+ 'label_placeholder' => 'Pole formuláře',
+
+ 'field_styling' => 'Vlastní CSS třídy',
+ 'field_styling_comment' => 'Můžete přidat vlastní styly',
+
+ 'autofocus' => 'Automaticky zvýraznit (autofocus)',
+ 'autofocus_comment' => 'Po zobrazení nastavit na poli kurzor',
+
+ 'wrapper_css' => 'CSS třída kontejneru',
+ 'wrapper_css_placeholder' => 'form-group',
+
+ 'field_css' => 'CSS třida pole',
+ 'field_css_placeholder' => 'form-control',
+
+ 'label_css' => 'CSS třída popisku (label)',
+ 'label_css_placeholder' => '',
+
+ 'field_validation' => 'Validační pravidla pole',
+ 'field_validation_comment' => 'Povolí nastavení vlastních validačních pravidel',
+
+ 'validation' => 'Pravidlo',
+ 'validation_prompt' => 'Přidat pravidlo',
+
+ 'validation_type' => 'Typ',
+
+ 'validation_error' => 'Chybová zpráva',
+ 'validation_error_placeholder' => 'Prosím vložte správná data.',
+ 'validation_error_comment' => 'Chybová hláška, která se zobrazí u pole',
+
+ 'validation_custom_type' => 'Název validačního pravidla',
+ 'validation_custom_type_comment' => 'Vložte název pravidla třídy Validator (např. regex, boolean, ...). Přehled validačních pravidel .',
+ 'validation_custom_type_placeholder' => 'regex',
+
+ 'validation_custom_pattern' => 'Podmínka validačního pravidla',
+ 'validation_custom_pattern_comment' => 'Nechte prázdné nebo doplňte podmínku pravidla (toto je pravá část zápisu validačního pravidla za dvojtečkou - např. [abc] pro pravidlo regex).',
+ 'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
+
+ 'custom' => 'Vlastní pole',
+ 'custom_description' => 'Vlastní pole s validačními pravidly',
+
+ 'add_values_prompt' => 'Přidat hodnoty',
+ 'field_value_id' => 'ID hodnoty',
+ 'field_value_content' => 'Obsah',
+
+ 'hit_type' => 'Hit type',
+ 'event_category' => 'Kategorie události (event category)',
+ 'event_action' => 'Akce události (event action)',
+ 'event_label' => 'Štítek události (event label)',
+
+ 'custom_code' => 'Vlastní kód',
+ 'custom_code_comment' => 'Vložený kód přepíše automaticky generovaný kód políčka formuláře. Používejte opatrně!',
+ 'custom_code_twig' => 'Povolit Twig',
+ 'custom_code_twig_comment' => 'Můžete povolit parset syntaxe jazyka Twig.',
+
+ 'custom_content' => 'Vlastní obsah',
+ 'custom_content_comment' => 'Obsah bude přidaný k políčku formuláře.',
+
+ ],
+
+ 'form_field_types' => [
+ 'text' => 'Text',
+ 'email' => 'Email',
+ 'textarea' => 'Textarea',
+ 'checkbox' => 'Checkbox',
+ 'dropdown' => 'Výběr (dropdown)',
+ 'file' => 'Soubor',
+ 'custom_code' => 'Vlastní kód',
+ 'custom_content' => 'Vlastní obsah',
+ ],
+
+ 'form_field_validation' => [
+ 'select' => '--- Vyberte pravidlo ---',
+ 'required' => 'Vyžadováno',
+ 'email' => 'Email',
+ 'numeric' => 'Číslo',
+ 'custom' => 'Vlastní pravidlo',
+ ],
+
+ 'email' => [
+ 'address_from' => 'Adresa OD',
+ 'address_from_placeholder' => 'john.doe@domain.com',
+
+ 'address_from_name' => 'Jméno odesílatele',
+ 'address_from_name_placeholder' => 'John Doe',
+
+ 'address_replyto' => 'Adresa Odpovědět na',
+ 'address_replyto_comment' => 'Odpověď na mail půjde na tuto adresu (REPLY-TO).',
+
+ 'subject' => 'Předmět emailu',
+ 'subject_comment' => 'Nastavte pouze pokud chcete přepsat předmět definovaný v šabloně (Nastavení > E-mailové šablony).',
+
+ 'template' => 'Šablona emailu',
+ 'template_comment' => 'Kód emailové šablony vytvořené v Nastavení > E-mailové šablony. Nechte prázdné pro výchozí šablonu: janvince.smallcontactform::mail.autoreply.',
+
+ 'allow_email_queue' => 'Řadit do fronty',
+ 'allow_email_queue_comment' => 'Přidat emaily do fronty místo okamžitého odeslání. Musíte ale nejdříve správně nakonfigurovat frontu systému OctoberCMS!',
+
+ 'allow_notifications' => 'Povolit odesílání upozornění',
+ 'allow_notifications_comment' => 'Odesílat upozornění, pokud někdo odešle formulář.',
+
+ 'notification_address_to' => 'Upozornění posílat na adresu:',
+ 'notification_address_to_comment' => 'Jedna emailová adresa nebo seznam adres oddělených čárkami',
+ 'notification_address_to_placeholder' => 'notifications@domain.com',
+
+ 'notification_address_from_form' => 'Nastavit adresu Od na email z formuláře (NEMUSÍ PODPOROVAT váš emailový systém!)',
+ 'notification_address_from_form_comment' => 'Nastaví u odesílaného upozornění adresu Od (From) na tu, která byla zadána ve formuláři (sloupec email musí mít nastavenou vazbu).',
+
+ 'allow_autoreply' => 'Povolit automatickou odpověď',
+ 'allow_autoreply_comment' => 'Poslat automatickou odpověď odesílateli formuláře',
+
+ 'autoreply_name_field' => 'Pole formuláře, které obsahuje JMÉNO odesílatele',
+ 'autoreply_name_field_empty_option' => '-- Vyberte --',
+ 'autoreply_name_field_comment' => 'Pole typu Text.',
+
+ 'autoreply_email_field' => 'Pole formuláře, které obsahuje ADRESU odesílatele',
+ 'autoreply_email_field_empty_option' => '-- Vyberte --',
+ 'autoreply_email_field_comment' => 'Pole typu Email.',
+
+ 'autoreply_message_field' => 'Pole formuláře, které obsahuje ZPRÁVU',
+ 'autoreply_message_field_empty_option' => '-- vyberte --',
+ 'autoreply_message_field_comment' => 'Pole typu Textarea nebo Text.',
+
+ 'notification_template' => 'Šablona notifikačního emailu',
+ 'notification_template_comment' => 'Kód emailové šablony vytvořené v Nastavení > E-mailové šablony. Nechte prázdné pro výchozí šablonu: janvince.smallcontactform::mail.notification.',
+
+ ],
+
+ 'antispam' => [
+ 'add_antispam' => 'Přidat pasivní ochranu proti spamu',
+ 'add_antispam_comment' => 'Přidá jednoduchou ale efektivní pasivní ochranu proti robotům (více informací v souboru README.md)',
+
+ 'antispam_delay' => 'Zpoždění formuláře (s)',
+ 'antispam_delay_comment' => 'Test na příliš rychlé odeslání formuláře (většinou roboty)',
+ 'antispam_delay_placeholder' => '3',
+
+ 'antispam_label' => 'Popisek (label) antispamového pole',
+ 'antispam_label_comment' => 'Popisek bude viditelný pouze na prohlížečích bez podpory JavaScriptu',
+ 'antispam_label_placeholder' => 'Prosím vymažte toto pole',
+
+ 'antispam_error_msg' => 'Chybová zprávy',
+ 'antispam_error_msg_comment' => 'Zpráva, která se zobrazí, pokud se aktivuje pasivní antispam',
+ 'antispam_error_msg_placeholder' => 'Prosím vymažte obsah tohoto pole!',
+
+ 'antispam_delay_error_msg' => 'Chybová zprávy při rychlém odeslání',
+ 'antispam_delay_error_msg_comment' => 'Zpráva, která se zobrazí při příliš rychlém odeslání formuláře',
+ 'antispam_delay_error_msg_placeholder' => 'Příliš rychlé odeslání formuláře! Prosím zkuste to za pár vteřin znovu!',
+
+ 'add_google_recaptcha' => 'Přidat Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Přidá reCaptcha do kontaktního formuláře (více informací v souboru README.md). API klíče můžete získat na stránce Google reCaptcha .',
+
+ 'google_recaptcha_version' => 'Verze Google reCaptcha',
+ 'google_recaptcha_version_comment' => 'Zvolte verzi reCaptcha widgetu. Více informací naleznete na webu Google reCaptcha .',
+
+ 'google_recaptcha_versions' => [
+ 'v2checkbox' => 'reCaptcha V2 zaškrtávací pole',
+ 'v2invisible' => 'reCaptcha V2 neviditelná',
+ ],
+
+ 'google_recaptcha_site_key' => 'Site key',
+ 'google_recaptcha_site_key_comment' => 'Vložte svůj "site key"',
+
+ 'google_recaptcha_secret_key' => 'Secret key',
+ 'google_recaptcha_secret_key_comment' => 'Vložte svůj "secret key"',
+
+ 'google_recaptcha_wrapper_css' => 'CSS třída kontejneru reCaptcha boxu',
+ 'google_recaptcha_wrapper_css_comment' => 'CSS třída kontejneru, ve kterém je vložený box reCaptcha',
+ 'google_recaptcha_wrapper_css_placeholder' => 'form-group',
+
+ 'google_recaptcha_error_msg' => 'Chybová zpráva',
+ 'google_recaptcha_error_msg_comment' => 'Zpráva, která se zobrazí, pokud dojde chybě při ověření reCAPTCHA.',
+ 'google_recaptcha_error_msg_placeholder' => 'Chyba při ověření pomocí Google reCAPTCHA!',
+
+ 'google_recaptcha_scripts_allow' => 'Automaticky přidat Google reCAPTCHA sckript',
+ 'google_recaptcha_scripts_allow_comment' => 'Vloží odkaz na JavaScriptový soubor potřebný pro fungování reCAPTCHA.',
+
+ 'google_recaptcha_locale_allow' => 'Povolit detekci jazyka',
+ 'google_recaptcha_locale_allow_comment' => 'Přidá k reCAPTCHA skriptu kód jazyka stránky, takže ověřovací box bude mluvit jazykem návštěvníka webu.',
+
+ 'add_ip_protection' => 'Testovat IP adresu odesílatele',
+ 'add_ip_protection_comment' => 'Nepovolí příliš mnoho odeslání formuláře z jedné IP adresy',
+
+ 'add_ip_protection_count' => 'Maximální počet odeslání během jednoho dne',
+ 'add_ip_protection_count_comment' => 'Počet povolených odeslání formuláře z jedné IP adresy během jednoho dne',
+ 'add_ip_protection_count_placeholder' => '3',
+
+ 'add_ip_protection_error_get_ip' => 'Nepodařilo se určit vaši IP adresu!',
+
+ 'add_ip_protection_error_too_many_submits' => 'Chybová zpráva při překročení počtu odeslání',
+ 'add_ip_protection_error_too_many_submits_comment' => 'Zpráva, kterou obdrží uživatel při překročení limitu počtu odeslání formuláře',
+ 'add_ip_protection_error_too_many_submits_placeholder' => 'Byl překročen limit odeslání formuláře během jednoho dne!',
+
+ 'disabled_extensions' => 'Zakázaná rozšíření',
+ 'disabled_extensions_comment' => 'Nastavení ze záložky Soukromí zkusobila vypnutí těchto rozšíření',
+ ],
+
+ 'mapping' => [
+
+ 'hint' => [
+ 'title' => 'Proč vazby na sloupce?',
+ 'content' => '
+ Můžete vytvořit libovolný formulář s vlastními poli a jejich typy.
+ Systém zapíše do databáze všechna odeslaná data formuláře, ale pro Přehled zpráv jsou zvlášť ukládána pole Jméno, Email a Zpráva.
+ Proto je nutné identifikovat pro tyto sloupce odpovídající pole ve vašem formuláři.
+ Vytvořené vazby jsou použité i při odesílání automatických odpovědí, kde je nutné vazba alespoň na pole Email.
+ ',
+ ],
+
+ 'warning' => [
+ 'title' => 'Nevidíte vaše formulářová pole?',
+ 'content' => '
+ Pokud zde nevidíte svá formulářová pole, klikněte dole na tlačítko Uložit a pak obnovte stránku (F5 nebo Ctr+R / Cmd+R).
+ ',
+ ],
+
+ ],
+
+ 'privacy' => [
+ 'disable_messages_saving' => 'Zakázat ukládání zpráv',
+ 'disable_messages_saving_comment' => 'Pokud je zaškrtnuto, odeslané zprávy se nebudou ukládat do databáze.Tato volba zároveň zakáže použití IP ochrany! ',
+ 'disable_messages_saving_comment_section' => '',
+ ],
+
+ 'tabs' => [
+ 'form' => 'Formulář',
+ 'buttons' => 'Odesílací tlačítko',
+ 'form_fields' => 'Pole formuláře',
+ 'mapping' => 'Vazby sloupců',
+ 'email' => 'Email',
+ 'antispam' => 'Antispam',
+ 'privacy' => 'Soukromí'
+ ],
+
+ ],
+
+ 'components' => [
+
+ 'groups' => [
+
+ 'hacks' => 'Hacks',
+ 'override_form' => 'Přepsat nastavení formuláře',
+ 'override_notifications' => 'Přepsat nastavení notifikací',
+ 'override_autoreply' => 'Přepsat nastavení automatických odpovědí',
+ 'override' => 'Přepsat nastavení',
+ 'override_redirect' => 'Přepsat nastavení přesměrování',
+ 'override_ga' => 'Přepsat nastavení Google Analytics',
+ ],
+
+ 'properties' => [
+
+ 'disable_notifications' => 'Zakázat odesílání notifikačních emailů',
+ 'disable_notifications_comment' => 'Zakáže odeslání notifikáčních emailů (bez ohledu na systémová nastavení formuláře)',
+
+ 'form_description' => 'Popisek formuláře',
+ 'form_description_comment' => 'Volitelně můžete přidat popisek formuláře, který se uloží společně s odeslanými daty do seznamu zpráv. Můžete použít i {{ :slug }}.',
+
+ 'disable_fields' => 'Zakázat pole',
+ 'disable_fields_comment' => 'Vložte názvy polí oddělené trubkou (např. name|message|phone)',
+
+ 'send_btn_label' => 'Popisek odesílacího tlačítka',
+ 'send_btn_label_comment' => 'Přepíše výchozí text odesílacího tlačítka',
+
+ 'form_success_msg' => 'Zpráva po úspěšném odeslání',
+ 'form_success_msg_comment' => 'Přepíše výchozí zprávu zobrazenou po úspěšném odeslání formuláře',
+
+ 'form_error_msg' => 'Zpráva po chybě při odeslání',
+ 'form_error_msg_comment' => 'Přepíše výchozí zprávu zobrazenou po neúspěšném odeslání formuláře',
+
+ 'notification_address_to' => 'Adresa KOMU',
+ 'notification_address_to_comment' => 'Přepíše adresu KOMU v notifikačním emailu',
+
+ 'notification_address_from' => 'Adresa OD',
+ 'notification_address_from_comment' => 'Přepíše adresu OD v notifikačním emailu',
+
+ 'notification_address_from_name' => 'Jméno pro adresu OD',
+ 'notification_address_from_name_comment' => 'Přepíše jméno zobrazené spolu s emailem OD',
+
+ 'notification_template' => 'Šablona notifikace',
+ 'notification_template_comment' => 'Přepíše šablonu notifikačního emailu',
+
+ 'notification_subject' => 'Předmět notifikace',
+ 'notification_template_comment' => 'Přepíše předmět emailu',
+
+ 'disable_autoreply' => 'Zakázat notifikace',
+ 'disable_autoreply_comment' => 'Zakáže odesílání notifikací',
+
+ 'autoreply_address_from' => 'Adresa OD',
+ 'autoreply_address_from_comment' => 'Přepíše adresu od v automatické odpovědi po odeslání formuláře',
+
+ 'autoreply_address_from_name' => 'Jméno pro adresu OD',
+ 'autoreply_address_from_name_comment' => 'Přepíše jméno zobrazené spolu s emailem OD',
+
+ 'autoreply_address_replyto' => 'Adresa ODPOVĚDĚT NA',
+ 'autoreply_address_replyto_comment' => 'Přepíše adresu REPLY TO v automatické odpovědi po odeslání formuláře.',
+
+ 'autoreply_template' => 'Šablona automatické odpovědi',
+ 'autoreply_template_comment' => 'Přepíše šablonu emailu automatické odpovědi',
+
+ 'autoreply_subject' => 'Předmět automatické odpovědi',
+ 'autoreply_template_comment' => 'Přepíše předmět emailu',
+
+ ]
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/de/lang.php b/plugins/janvince/smallcontactform/lang/de/lang.php
new file mode 100644
index 000000000..1e023701c
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/de/lang.php
@@ -0,0 +1,491 @@
+ [
+ 'name' => 'Kontaktformular',
+ 'description' => 'Kontaktformular',
+ 'category' => 'Kontaktformular',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Auf Nachrichtenliste zugreifen',
+ 'access_settings' => 'Backendeinstellungen bearbeiten',
+ 'delete_messages' => 'gespeicherte Nachrichten löschen',
+ 'export_messages' => 'gespeicherte Nachrichten exportieren',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Kontaktformular',
+ 'messages' => 'Nachrichten',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Kontaktformular',
+ 'description' => 'Kontaktformular in die Seite einfügen',
+ 'no_fields' => 'Bitte fügen Sie zuerst einige Formularfelder in der Backend-Administration hinzu (in Einstellungen > Kontaktformular > Felder)...',
+ ],
+
+ 'filter' => [
+ 'date' => 'Datumsbereich',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Nachrichten',
+ 'latest_record' => 'Letzte von',
+ 'new_count' => 'Neu',
+ 'new_description' => 'Nachrichten',
+ 'read_count' => 'Gelesen',
+ 'all_count' => 'Insgesamt',
+ 'all_description' => 'Nachrichten',
+ 'settings_btn' => 'Formular Einstellungen',
+ 'mark_read' => 'Als Gelesen markieren',
+ 'mark_read_confirm' => 'Möchten Sie wirklich die ausgewählten Nachrichten als gelesen markeiren?',
+ 'mark_read_success' => 'Erfolgreich als gelesen markiert.',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Nachricht nicht gefunden!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Datum und Zeit',
+ 'form_data' => 'Daten',
+ 'name' => 'Name',
+ 'email' => 'Email',
+ 'message' => 'Nachricht',
+ 'new_message' => 'Status',
+ 'new' => 'Neu',
+ 'read' => 'Gelesen',
+ 'remote_ip' => 'IP des Absenders',
+ 'form_alias' => 'Alias',
+ 'form_description' => 'Beschreibung',
+ 'created_at' => 'Erstellt am',
+ 'updated_at' => 'Geupdated am',
+ ]
+
+ ],
+
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+
+ 'list_title' => 'Nachrichten',
+ 'preview' => 'Vorschau',
+ 'preview_title' => 'Nachricht',
+ 'preview_date' => 'Datum:',
+ 'preview_content_title' => 'Inhalt:',
+ 'remote_ip' => 'Von IP gesendet:',
+ 'export' => 'Exportieren',
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Unerlaubter Zugriff',
+ ],
+
+ ],
+
+ 'mail' => [
+
+ 'templates' => [
+
+ 'autoreply' => 'Form autoreply message (English)',
+ 'autoreply_cs' => 'Form autoreply message (Czech)',
+
+ 'notification' => 'Form notification message (English)',
+ 'notification_cs' => 'Form notification message (Czech)',
+
+ ]
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+
+ 'messages' => [
+ 'label' => 'Kontaktformular - Nachrichtenstatistik',
+ 'title' => 'Nachrichtenstatistik',
+ 'messages_all' => 'Alle',
+ 'messages_new' => 'Neu',
+ 'messages_read' => 'Gelesen',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Kontaktformular - Neue Nachrichten',
+ 'title' => 'Neue Nachrichten',
+ 'link_text' => 'Hier klicken, um die Liste aller Nachrichten anzuzeigen',
+ ],
+
+ ],
+
+ ],
+
+ 'settings' => [
+
+ 'form' => [
+
+ 'css_class' => 'Formular CSS Klasse',
+
+ 'use_placeholders' => 'Platzhalter benutzen',
+ 'use_placeholders_comment' => 'Platzhalter werden anstelle von Labels verwendet',
+
+ 'disable_browser_validation' => 'Browservalidierung deaktivieren',
+ 'disable_browser_validation_comment' => 'Integrierte Validierung und Popups im Browser nicht zulassen.',
+
+ 'success_msg' => 'Form success message',
+ 'success_msg_placeholder' => 'Wir haben Ihre Nachricht erhalten.',
+
+ 'error_msg' => 'Form error message',
+ 'error_msg_placeholder' => 'Es gab einen Fehler beim Senden Ihrer Daten!',
+
+ 'allow_ajax' => 'Enable AJAX',
+ 'allow_ajax_comment' => 'AJAX mit Fallback für Browser ohne JavaScript verwenden',
+
+ 'allow_confirm_msg' => 'Vor dem Absenden des Formulars um Bestätigung bitten',
+ 'allow_confirm_msg_comment' => 'Bestätigungsdialog vor dem Senden hinzufügen',
+
+ 'send_confirm_msg' => 'Bestätigungs-Text',
+ 'send_confirm_msg_placeholder' => 'Sind Sie sicher?',
+
+ 'hide_after_success' => 'Formular nach erfolgreichem Senden ausblenden',
+ 'hide_after_success_comment' => 'Nur Erfolgsmeldung ohne Formular anzeigen',
+
+ 'add_assets' => 'Assets hinzufügen',
+ 'add_assets_comment' => 'Automatisches Hinzufügen notwendiger CSS- und JS-Assets (mehr über Assets in der Datei README.md)',
+
+ 'add_css_assets' => 'CSS Assets hinzufügen',
+ 'add_css_assets_comment' => 'Alle benötigten CSS Dateien werden hinzugefügt',
+
+ 'add_js_assets' => 'JavaScript Assets hinzufügen',
+ 'add_js_assets_comment' => 'Alle benötigten JavaScript Dateien werden hinzugefügt',
+
+
+ ],
+
+ 'buttons' => [
+ 'send_btn_text' => 'Text senden Button',
+ 'send_btn_text_placeholder' => 'Absenden',
+
+ 'send_btn_css_class' => 'CSS Klasse senden Button',
+ 'send_btn_css_class_placeholder' => 'btn btn-primary',
+
+ 'send_btn_wrapper_css' => 'CSS Klasse des senden Button wrappers',
+ 'send_btn_wrapper_css_placeholder' => 'form-group',
+
+ ],
+
+ 'redirect' => [
+
+ 'allow_redirect' => 'Nach dem Einreichen umleiten',
+ 'allow_redirect_comment' => 'Nach erfolgreicher Übermittlung auf eine andere Seite umleiten',
+
+ 'redirect_url' => 'URL der Seite, auf die umgeleitet werden soll',
+ 'redirect_url_comment' => 'Geben Sie die URL Ihrer Seite ein (bspw. /kontact/danke)',
+ 'redirect_url_placeholder' => '/kontakt/danke',
+
+ 'redirect_url_external' => 'Externe URL',
+ 'redirect_url_external_comment' => 'Dies ist ein externer URL-Pfad (bspw. http://www.domain.com',
+
+ ],
+
+ 'form_fields' => [
+ 'prompt' => 'Neues Formularfeld hinzufügen',
+
+ 'name' => 'FELDNAME',
+ 'name_comment' => 'Kleinbuchstaben ohne Sonderzeichen (bspw. name, email, adresse, ...)',
+
+ 'type' => 'Feldtyp',
+
+ 'label' => 'Label',
+ 'label_placeholder' => 'Vollständiger Name',
+
+ 'field_styling' => 'Eigene CSS Klasse',
+ 'field_styling_comment' => 'Ändern der Standard-Bootstrap-Stile',
+
+ 'autofocus' => 'Autofokus-Feld',
+ 'autofocus_comment' => 'Autofokus für dieses Formularfeld',
+
+ 'wrapper_css' => 'Wrapper CSS-Klasse',
+ 'wrapper_css_placeholder' => 'form-group',
+
+ 'field_css' => 'Feld CSS-Klasse',
+ 'field_css_placeholder' => 'form-control',
+
+ 'label_css' => 'Label CSS-Klasse',
+ 'label_css_placeholder' => '',
+
+ 'field_validation' => 'Feldüberprüfung',
+ 'field_validation_comment' => 'Feldüberprüfungsregeln hinzufügen',
+
+ 'validation' => 'Validierung',
+ 'validation_prompt' => 'Validierung hinzufügen',
+
+ 'validation_type' => 'Validierungsregel',
+
+ 'validation_error' => 'Validierungs-Fehlermeldung',
+ 'validation_error_placeholder' => 'Bitte gültige Daten eingeben.',
+ 'validation_error_comment' => 'Fehlermeldung, die zu verwenden ist, wenn die Validierung fehlschlägt',
+
+ 'validation_custom_type' => 'Name der Validierungsregel',
+ 'validation_custom_type_comment' => 'Validator-Regelnamen eingeben (bspw. regex, boolean, ...). See validation rules .',
+ 'validation_custom_type_placeholder' => 'regex',
+
+ 'validation_custom_pattern' => 'Muster für Validierungsregeln',
+ 'validation_custom_pattern_comment' => 'Left empty or enter custom rule pattern (this is a right part of Validator rule after colon - eg. [abc] for regex).',
+ 'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
+
+ 'custom' => 'Benutzerdefiniertes Feld',
+ 'custom_description' => 'Benutzerdefiniertes Feld mit Validierungsoption',
+
+ 'add_values_prompt' => 'Werte hinzufügen',
+ 'field_value_id' => 'Feld ID',
+ 'field_value_content' => 'Feld Inhalt',
+
+ ],
+
+ 'form_field_types' => [
+ 'text' => 'Text',
+ 'email' => 'Email',
+ 'textarea' => 'Textbereich',
+ 'checkbox' => 'Checkbox',
+ 'dropdown' => 'Dropdown-Menü',
+ 'file' => 'File',
+ 'custom_code' => 'Custom code',
+ 'custom_content' => 'Custom content',
+ ],
+
+ 'form_field_validation' => [
+ 'select' => '--- Validierung auswählen ---',
+ 'required' => 'erforderlich',
+ 'email' => 'Email',
+ 'numeric' => 'Numerisch',
+ 'custom' => 'Benutzerdefinierte Regel',
+ ],
+
+ 'email' => [
+ 'address_from' => 'Absenderaddresse',
+ 'address_from_placeholder' => 'max.mustermann@domain.de',
+
+ 'address_from_name' => 'Absendername',
+ 'address_from_name_placeholder' => 'Max Mustermann',
+
+ 'subject' => 'Email subject',
+ 'subject_comment' => 'Nur einstellen, wenn Sie andere als die unter Einstellungen > Mail-Vorlagen definierten Einstellungen wünschen.',
+
+ 'template' => 'Email template',
+ 'template_comment' => 'Code der E-Mail-Vorlage, die unter Einstellungen > E-Mail-Vorlagen erstellt wurde. Bei Standardvorlage leer lassen: janvince.smallcontactform::mail.autoreply.',
+
+ 'allow_email_queue' => 'E-Mail in Warteschlange setzen',
+ 'allow_email_queue_comment' => 'Fügen Sie E-Mails in die Warteschlange ein, anstatt sie sofort zu senden. Sie müssen zuerst Ihre OctoberCMS-Warteschlange konfigurieren!',
+
+ 'allow_notifications' => 'Benachrichtigungen zulassen',
+ 'allow_notifications_comment' => 'Benachrichtigung senden, nachdem das Formular gesendet wurde',
+
+ 'notification_address_to' => 'Benachrichtigung an E-Mail senden',
+ 'notification_address_to_comment' => 'Eine E-Mail-Adresse oder eine durch Komma getrennte Liste von Adressen',
+ 'notification_address_to_placeholder' => 'kontakt@domain.de',
+
+ 'notification_address_from_form' => 'Benachrichtigung von der Adresse des Absenders erzwingen (NICHT von allen E-Mail-Systemen UNTERSTÜTZT!)',
+ 'notification_address_from_form_comment' => 'Benachrichtigung von Adresse auf eine im Kontaktformular eingegebene E-Mail setzen (das Feld muss in der Spaltenzuordnung gesetzt werden).',
+
+ 'allow_autoreply' => 'Autoreply zulassen',
+ 'allow_autoreply_comment' => 'Senden Sie eine Kopie des Formularinhalts an den Autor',
+
+ 'autoreply_name_field' => 'NAME form field',
+ 'autoreply_name_field_empty_option' => '-- Select --',
+ 'autoreply_name_field_comment' => 'Must be type of Text.Save and refresh this page if you can\'t see your fields. ',
+
+ 'autoreply_email_field' => 'EMAIL address form field',
+ 'autoreply_email_field_empty_option' => '-- Select --',
+ 'autoreply_email_field_comment' => 'Must be type of Email.Save and refresh this page if you can\'t see your fields. ',
+
+ 'autoreply_message_field' => 'MESSAGE form field',
+ 'autoreply_message_field_empty_option' => '-- Select --',
+ 'autoreply_message_field_comment' => 'Must be type of Textarea or Text.Save and refresh this page if you can\'t see your fields. ',
+
+ 'notification_template' => 'Notification email template',
+ 'notification_template_comment' => 'Code of email template created in Settings > Email templates. Left empty for default template: janvince.smallcontactform::mail.autoreply.',
+
+ ],
+
+ 'antispam' => [
+ 'add_antispam' => 'Add passive antispam protection',
+ 'add_antispam_comment' => 'Add simple but effective passive antispam control (more info in README.md file)',
+
+ 'antispam_delay' => 'Antispam delay (s)',
+ 'antispam_delay_comment' => 'Delay protection for too fast form sending (usually by robots)',
+ 'antispam_delay_placeholder' => '3',
+
+ 'antispam_label' => 'Antispam field label',
+ 'antispam_label_comment' => 'Label will be visible for non JavaScript enabled browsers',
+ 'antispam_label_placeholder' => 'Please clear this field',
+
+ 'antispam_error_msg' => 'Error message',
+ 'antispam_error_msg_comment' => 'Message to show to user when antispam protection is triggered',
+ 'antispam_error_msg_placeholder' => 'Please empty this field!',
+
+ 'antispam_delay_error_msg' => 'Delay error message',
+ 'antispam_delay_error_msg_comment' => 'Message to show to user when form was sent too fast',
+ 'antispam_delay_error_msg_placeholder' => 'Form sent too fast! Please wait few seconds and try again!',
+
+ 'add_google_recaptcha' => 'Add Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Add reCaptcha to Contact Form (more info in README.md file). You can get API keys on Google reCaptcha site .',
+
+ 'google_recaptcha_version' => 'Google reCaptcha version',
+ 'google_recaptcha_version_comment' => 'Choose a version of reCaptcha widget. More info on Google reCaptcha site .',
+
+ 'google_recaptcha_versions' => [
+ 'v2checkbox' => 'reCaptcha V2 checkbox',
+ 'v2invisible' => 'reCaptcha V2 invisible',
+ ],
+
+ 'google_recaptcha_site_key' => 'Site key',
+ 'google_recaptcha_site_key_comment' => 'Put your site key',
+
+ 'google_recaptcha_secret_key' => 'Secret key',
+ 'google_recaptcha_secret_key_comment' => 'Put your secret key',
+
+ 'google_recaptcha_wrapper_css' => 'reCaptcha box wrapper CSS class',
+ 'google_recaptcha_wrapper_css_comment' => 'CSS class of wrapper box around reCaptcha box',
+ 'google_recaptcha_wrapper_css_placeholder' => 'form-group',
+
+ 'google_recaptcha_error_msg' => 'Error message',
+ 'google_recaptcha_error_msg_comment' => 'Message to show to user when reCAPTCHA is not validated.',
+ 'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA validation error!',
+
+ 'google_recaptcha_scripts_allow' => 'Automatically add necessary JS scripts',
+ 'google_recaptcha_scripts_allow_comment' => 'This will add link to JS scripts to your site.',
+
+ 'google_recaptcha_locale_allow' => 'Allow locale detection',
+ 'google_recaptcha_locale_allow_comment' => 'This will add curent web page locale to reCAPTCHA script, so it will translated.',
+
+ 'add_ip_protection' => 'Check sender\'s IP',
+ 'add_ip_protection_comment' => 'Do not allow too many form submits from one IP address',
+
+ 'add_ip_protection_count' => 'Maximum form submits during a day',
+ 'add_ip_protection_count_comment' => 'Number of allowed submits from one IP address during a single day',
+ 'add_ip_protection_count_placeholder' => '3',
+
+ 'add_ip_protection_error_get_ip' => 'We wasn\'t able to determine your IP address!',
+
+ 'add_ip_protection_error_too_many_submits' => 'Too many submits error message',
+ 'add_ip_protection_error_too_many_submits_comment' => 'Error message to show to the user',
+ 'add_ip_protection_error_too_many_submits_placeholder' => 'Too many form submits from one address today!',
+
+ 'disabled_extensions' => 'Disabled extensions',
+ 'disabled_extensions_comment' => 'Settings set on Privacy tab disabled these extensions',
+
+ ],
+
+ 'mapping' => [
+
+ 'hint' => [
+ 'title' => 'Why fields mapping?',
+ 'content' => '
+ You can build a custom form with own field names and types.
+ System writes all form data in database, but for quick overview Name, Email and Message columns are visible separately in Messages list.
+ So you have to help system to identify these columns by mapping to your form fields.
+ These mappings are also used for autoreply emails where at least Email field mapping is important.
+ ',
+ ],
+
+ 'warning' => [
+ 'title' => 'Can\'t select your form fields?',
+ 'content' => '
+ If you don\'t see your form fields, click on button Save at the bottom of this page and then reload page (F5 or Ctr+R / Cmd+R).
+ ',
+ ],
+
+ ],
+
+ 'privacy' => [
+ 'disable_messages_saving' => 'Disable messages saving',
+ 'disable_messages_saving_comment' => 'When checked, no data will saved in Messages list.This will also disable IP protection! ',
+ 'disable_messages_saving_comment_section' => '',
+ ],
+
+ 'tabs' => [
+ 'form' => 'Form',
+ 'buttons' => 'Send button',
+ 'form_fields' => 'Fields',
+ 'mapping' => 'Columns mapping',
+ 'email' => 'Email',
+ 'antispam' => 'Antispam',
+ 'privacy' => 'Privacy'
+ ],
+
+ ],
+
+ 'components' => [
+
+ 'groups' => [
+
+ 'hacks' => 'Hacks',
+ 'override_form' => 'Override form settings',
+ 'override_notifications' => 'Override notification settings',
+ 'override_autoreply' => 'Override autoreply settings',
+ 'override' => 'Override form settings',
+
+
+ ],
+ 'properties' => [
+
+ 'form_description' => 'Form description',
+ 'form_description_comment' => 'You can add optional form description, that will be saved with other sent data in the messages list. You can also use {{ :slug }} here.',
+
+ 'disable_fields' => 'Disable fields',
+ 'disable_fields_comment' => 'This will disable listed fields. Add field names separated by pipe (eg. name|message|phone)',
+
+ 'send_btn_label' => 'Send button label',
+ 'send_btn_label_comment' => 'Override send button label',
+
+ 'form_success_msg' => 'Success message',
+ 'form_success_msg_comment' => 'Override success message shown after successful sent',
+
+ 'form_error_msg' => 'Error message',
+ 'form_error_msg_comment' => 'Override error message shown after unsuccessful sent',
+
+ 'disable_notifications' => 'Disable notification',
+ 'disable_notifications_comment' => 'This will disable notification emails (overrides form settings)',
+
+ 'notification_address_to' => 'Address TO',
+ 'notification_address_to_comment' => 'This will override email address where notification email will be sent (if enabled in form settings)',
+
+ 'notification_address_from' => 'Address FROM',
+ 'notification_address_from_comment' => 'This will override email address from where notification email will be sent',
+
+ 'notification_address_from_name' => 'Address FROM name',
+ 'notification_address_from_name_comment' => 'This will override email address name from where notification email will be sent',
+
+ 'notification_template' => 'Notification template',
+ 'notification_template_comment' => 'This will override notification email template (eg. janvince.smallcontactform::mail.notification)',
+
+ 'disable_autoreply' => 'Disable notification',
+ 'disable_autoreply_comment' => 'This will disable notification emails (overrides form settings)',
+
+ 'autoreply_address_from' => 'Address FROM',
+ 'autoreply_address_from_comment' => 'This will override email address in autoreply email (if enabled in form settings)',
+
+ 'autoreply_address_from_name' => 'Address (FROM) name',
+ 'autoreply_address_from_name_comment' => 'This will override email address name in autoreply email (if enabled in form settings)',
+
+ 'autoreply_template' => 'Autoreply template',
+ 'autoreply_template_comment' => 'This will override autoreply email template (eg. janvince.smallcontactform::mail.autoreply)',
+
+ ]
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/en/lang.php b/plugins/janvince/smallcontactform/lang/en/lang.php
new file mode 100644
index 000000000..7ee282274
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/en/lang.php
@@ -0,0 +1,529 @@
+ [
+ 'name' => 'Contact form',
+ 'description' => 'Simple contact form builder',
+ 'category' => 'Small plugins',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Access messages list',
+ 'access_settings' => 'Manage backend preferences',
+ 'delete_messages' => 'Delete stored messages',
+ 'export_messages' => 'Export messages',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Contact form',
+ 'messages' => 'Messages',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Contact form',
+ 'description' => 'Insert contact form to the page',
+ 'no_fields' => 'Please add some form fields in backend administration first (in Settings > Small Contact form > Fields)...',
+ ],
+
+ 'filter' => [
+ 'date' => 'Date range',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Messages',
+ 'latest_record' => 'Latest from',
+ 'new_count' => 'New',
+ 'new_description' => 'Messages',
+ 'read_count' => 'Read',
+ 'all_count' => 'Total',
+ 'all_description' => 'Messages',
+ 'settings_btn' => 'Form settings',
+ 'mark_read' => 'Mark as read',
+ 'mark_read_confirm' => 'Really set selected messages as read?',
+ 'mark_read_success' => 'Successfully marked as read.',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Message not found!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Date and time',
+ 'form_data' => 'Form data',
+ 'name' => 'Name',
+ 'email' => 'Email',
+ 'message' => 'Message',
+ 'new_message' => 'Status',
+ 'new' => 'New',
+ 'read' => 'Read',
+ 'remote_ip' => 'Sender\'s IP',
+ 'form_alias' => 'Alias',
+ 'form_description' => 'Description',
+ 'created_at' => 'Created at',
+ 'updated_at' => 'Updated at',
+ 'url' => 'URL',
+ 'files' => 'Files',
+ ]
+
+ ],
+
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+
+ 'list_title' => 'Messages',
+ 'preview' => 'Preview',
+ 'preview_title' => 'Contact form message',
+ 'preview_date' => 'From date:',
+ 'preview_content_title' => 'Content:',
+ 'remote_ip' => 'Sent from ip',
+ 'export' => 'Export',
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Unauthorized access',
+ ],
+
+ ],
+
+ 'mail' => [
+
+ 'templates' => [
+
+ 'autoreply' => 'Form autoreply message (English)',
+ 'autoreply_cs' => 'Form autoreply message (Czech)',
+
+ 'notification' => 'Form notification message (English)',
+ 'notification_cs' => 'Form notification message (Czech)',
+
+ ]
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+
+ 'messages' => [
+ 'label' => 'Contact form - Messages stats',
+ 'title' => 'Messages stats',
+ 'messages_all' => 'All',
+ 'messages_new' => 'New',
+ 'messages_read' => 'Read',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Contact form - New messages',
+ 'title' => 'New messages',
+ 'link_text' => 'Click to show Messages list',
+ ],
+
+ ],
+
+ ],
+
+ 'settings' => [
+
+ 'form' => [
+
+ 'css_class' => 'Form CSS class',
+
+ 'use_placeholders' => 'Use placeholders',
+ 'use_placeholders_comment' => 'Placeholders will be shown instead of field labels',
+
+ 'disable_browser_validation' => 'Disable browser validation',
+ 'disable_browser_validation_comment' => 'Do not allow browser built-in validation and popups.',
+
+ 'success_msg' => 'Form success message',
+ 'success_msg_placeholder' => 'Your data was sent.',
+
+ 'error_msg' => 'Form error message',
+ 'error_msg_placeholder' => 'There was an error sending your data!',
+
+ 'allow_ajax' => 'Enable AJAX',
+ 'allow_ajax_comment' => 'Allow AJAX with fallback for non JavaScript browsers',
+
+ 'allow_confirm_msg' => 'Ask confirmation before form send',
+ 'allow_confirm_msg_comment' => 'Add confirm dialog before sending',
+
+ 'send_confirm_msg' => 'Confirmation text',
+ 'send_confirm_msg_placeholder' => 'Are you sure?',
+
+ 'hide_after_success' => 'Hide form after successful send',
+ 'hide_after_success_comment' => 'Show only success message without form',
+
+ 'add_assets' => 'Add assets',
+ 'add_assets_comment' => 'Automatically add necessary CSS and JS assets (more about assets in README.md file)',
+
+ 'add_css_assets' => 'Add CSS assets',
+ 'add_css_assets_comment' => 'All necesssary styles will be included',
+
+ 'add_js_assets' => 'Add JavaScript assets',
+ 'add_js_assets_comment' => 'All necesssary JavaScripts will be included',
+
+ 'form_ga_event_success' => 'GA event after successful sent',
+
+ ],
+
+ 'sections' => [
+ 'ga_events' => 'Events'
+ ],
+
+ 'ga' => [
+ 'ga_success_event_allow' => 'Send event after successful sent',
+
+ ],
+
+ 'buttons' => [
+ 'send_btn_text' => 'Send button text',
+ 'send_btn_text_placeholder' => 'Send',
+
+ 'send_btn_css_class' => 'Send button CSS class',
+ 'send_btn_css_class_placeholder' => 'btn btn-primary',
+
+ 'send_btn_wrapper_css' => 'Send button wrapper CSS class',
+ 'send_btn_wrapper_css_placeholder' => 'form-group',
+
+ ],
+
+ 'redirect' => [
+
+ 'allow_redirect' => 'Redirect after submit',
+ 'allow_redirect_comment' => 'Redirect to another page after successfull submit',
+
+ 'redirect_url' => 'Page URL to redirect to',
+ 'redirect_url_comment' => 'Enter your page URL (eg. /contact/thank-you)',
+ 'redirect_url_placeholder' => '/contact/thank-you',
+
+ 'redirect_url_external' => 'External URL',
+ 'redirect_url_external_comment' => 'This is external URL path (eg. http://www.domain.com)',
+
+ ],
+
+ 'form_fields' => [
+ 'prompt' => 'Add new form field',
+
+ 'name' => 'FIELD NAME',
+ 'name_comment' => 'Lower case without special characters (eg. name, email, home_address, ...)',
+
+ 'type' => 'Field type',
+
+ 'label' => 'Label',
+ 'label_placeholder' => 'Full name',
+
+ 'field_styling' => 'Custom CSS class',
+ 'field_styling_comment' => 'Change default Bootstrap styles',
+
+ 'autofocus' => 'Autofocus field',
+ 'autofocus_comment' => 'Autofocus this form field',
+
+ 'wrapper_css' => 'Wrapper CSS class',
+ 'wrapper_css_placeholder' => 'form-group',
+
+ 'field_css' => 'Field CSS class',
+ 'field_css_placeholder' => 'form-control',
+
+ 'label_css' => 'Label CSS class',
+ 'label_css_placeholder' => '',
+
+ 'field_validation' => 'Field validation',
+ 'field_validation_comment' => 'Add field validation rules',
+
+ 'validation' => 'Validation',
+ 'validation_prompt' => 'Add validation',
+
+ 'validation_type' => 'Validation rule',
+
+ 'validation_error' => 'Validation error message',
+ 'validation_error_placeholder' => 'Please enter valid data.',
+ 'validation_error_comment' => 'Error message to use when validation fails',
+
+ 'validation_custom_type' => 'Validation rule name',
+ 'validation_custom_type_comment' => 'Enter Validator rule name (eg. regex, boolean, ...). See validation rules .',
+ 'validation_custom_type_placeholder' => 'regex',
+
+ 'validation_custom_pattern' => 'Validation rule pattern',
+ 'validation_custom_pattern_comment' => 'Left empty or enter custom rule pattern (this is a right part of Validator rule after colon - eg. [abc] for regex).',
+ 'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
+
+ 'custom' => 'Custom field',
+ 'custom_description' => 'Custom field with validation option',
+
+ 'add_values_prompt' => 'Add values',
+ 'field_value_id' => 'Field value ID',
+ 'field_value_content' => 'Field value content',
+
+ 'hit_type' => 'Hit type',
+ 'event_category' => 'Event category',
+ 'event_action' => 'Event action',
+ 'event_label' => 'Event label',
+
+ 'custom_code' => 'Custom code',
+ 'custom_code_comment' => 'This code will override built in field code. Use carefully!',
+ 'custom_code_twig' => 'Allow Twig',
+ 'custom_code_twig_comment' => 'If checked, Twig markup will be parsed.',
+
+ 'custom_content' => 'Custom content',
+ 'custom_content_comment' => 'This content will be added to field.',
+ ],
+
+ 'form_field_types' => [
+ 'text' => 'Text',
+ 'email' => 'Email',
+ 'textarea' => 'Textarea',
+ 'checkbox' => 'Checkbox',
+ 'dropdown' => 'Dropdown',
+ 'file' => 'File',
+ 'custom_code' => 'Custom code',
+ 'custom_content' => 'Custom content',
+ ],
+
+ 'form_field_validation' => [
+ 'select' => '--- Select validation ---',
+ 'required' => 'Required',
+ 'email' => 'Email',
+ 'numeric' => 'Numeric',
+ 'custom' => 'Custom rule',
+ ],
+
+ 'email' => [
+ 'address_from' => 'From address',
+ 'address_from_placeholder' => 'john.doe@domain.com',
+
+ 'address_from_name' => 'From address name',
+ 'address_from_name_placeholder' => 'John Doe',
+
+ 'address_replyto' => 'Reply To address',
+ 'address_replyto_comment' => 'Reply to mail will be send to this address.',
+
+ 'subject' => 'Email subject',
+ 'subject_comment' => 'Set only if you want other than defined in Settings > Mail templates.',
+
+ 'template' => 'Email template',
+ 'template_comment' => 'Code of email template created in Settings > Email templates. Left empty for default template: janvince.smallcontactform::mail.autoreply.',
+
+ 'allow_email_queue' => 'Queueing mail',
+ 'allow_email_queue_comment' => 'Add email to queue instead of immediately send. You have to configure your OctoberCMS queue first!',
+
+ 'allow_notifications' => 'Allow notifications',
+ 'allow_notifications_comment' => 'Send notification after form has been sent',
+
+ 'notification_address_to' => 'Send notification to email',
+ 'notification_address_to_comment' => 'One email address or comma-separated list of addresses',
+ 'notification_address_to_placeholder' => 'notifications@domain.com',
+
+ 'notification_address_from_form' => 'Force notification From address (NOT SUPPORTED by all email systems!)',
+ 'notification_address_from_form_comment' => 'Set notification From address to an email entered in contact form (the field must be set in column mapping).',
+
+ 'allow_autoreply' => 'Allow autoreply',
+ 'allow_autoreply_comment' => 'Send a form content copy to author',
+
+ 'autoreply_name_field' => 'NAME form field',
+ 'autoreply_name_field_empty_option' => '-- Select --',
+ 'autoreply_name_field_comment' => 'Must be type of Text.Save and refresh this page if you can\'t see your fields. ',
+
+ 'autoreply_email_field' => 'EMAIL address form field',
+ 'autoreply_email_field_empty_option' => '-- Select --',
+ 'autoreply_email_field_comment' => 'Must be type of Email.Save and refresh this page if you can\'t see your fields. ',
+
+ 'autoreply_message_field' => 'MESSAGE form field',
+ 'autoreply_message_field_empty_option' => '-- Select --',
+ 'autoreply_message_field_comment' => 'Must be type of Textarea or Text.Save and refresh this page if you can\'t see your fields. ',
+
+ 'notification_template' => 'Notification email template',
+ 'notification_template_comment' => 'Code of email template created in Settings > Email templates. Left empty for default template: janvince.smallcontactform::mail.autoreply.',
+
+ ],
+
+ 'antispam' => [
+ 'add_antispam' => 'Add passive antispam protection',
+ 'add_antispam_comment' => 'Add simple but effective passive antispam control (more info in README.md file)',
+
+ 'antispam_delay' => 'Antispam delay (s)',
+ 'antispam_delay_comment' => 'Delay protection for too fast form sending (usually by robots)',
+ 'antispam_delay_placeholder' => '3',
+
+ 'antispam_label' => 'Antispam field label',
+ 'antispam_label_comment' => 'Label will be visible for non JavaScript enabled browsers',
+ 'antispam_label_placeholder' => 'Please clear this field',
+
+ 'antispam_error_msg' => 'Error message',
+ 'antispam_error_msg_comment' => 'Message to show to user when antispam protection is triggered',
+ 'antispam_error_msg_placeholder' => 'Please empty this field!',
+
+ 'antispam_delay_error_msg' => 'Delay error message',
+ 'antispam_delay_error_msg_comment' => 'Message to show to user when form was sent too fast',
+ 'antispam_delay_error_msg_placeholder' => 'Form sent too fast! Please wait few seconds and try again!',
+
+ 'add_google_recaptcha' => 'Add Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Add reCaptcha to Contact Form (more info in README.md file). You can get API keys on Google reCaptcha site .',
+
+ 'google_recaptcha_version' => 'Google reCaptcha version',
+ 'google_recaptcha_version_comment' => 'Choose a version of reCaptcha widget. More info on Google reCaptcha site .',
+
+ 'google_recaptcha_versions' => [
+ 'v2checkbox' => 'reCaptcha V2 checkbox',
+ 'v2invisible' => 'reCaptcha V2 invisible',
+ ],
+
+ 'google_recaptcha_site_key' => 'Site key',
+ 'google_recaptcha_site_key_comment' => 'Put your site key',
+
+ 'google_recaptcha_secret_key' => 'Secret key',
+ 'google_recaptcha_secret_key_comment' => 'Put your secret key',
+
+ 'google_recaptcha_wrapper_css' => 'reCaptcha box wrapper CSS class',
+ 'google_recaptcha_wrapper_css_comment' => 'CSS class of wrapper box around reCaptcha box',
+ 'google_recaptcha_wrapper_css_placeholder' => 'form-group',
+
+ 'google_recaptcha_error_msg' => 'Error message',
+ 'google_recaptcha_error_msg_comment' => 'Message to show to user when reCAPTCHA is not validated.',
+ 'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA validation error!',
+
+ 'google_recaptcha_scripts_allow' => 'Automatically add necessary JS scripts',
+ 'google_recaptcha_scripts_allow_comment' => 'This will add link to JS scripts to your site.',
+
+ 'google_recaptcha_locale_allow' => 'Allow locale detection',
+ 'google_recaptcha_locale_allow_comment' => 'This will add curent web page locale to reCAPTCHA script, so it will translated.',
+
+ 'add_ip_protection' => 'Check sender\'s IP',
+ 'add_ip_protection_comment' => 'Do not allow too many form submits from one IP address',
+
+ 'add_ip_protection_count' => 'Maximum form submits during a day',
+ 'add_ip_protection_count_comment' => 'Number of allowed submits from one IP address during a single day',
+ 'add_ip_protection_count_placeholder' => '3',
+
+ 'add_ip_protection_error_get_ip' => 'We wasn\'t able to determine your IP address!',
+
+ 'add_ip_protection_error_too_many_submits' => 'Too many submits error message',
+ 'add_ip_protection_error_too_many_submits_comment' => 'Error message to show to the user',
+ 'add_ip_protection_error_too_many_submits_placeholder' => 'Too many form submits from one address today!',
+
+ 'disabled_extensions' => 'Disabled extensions',
+ 'disabled_extensions_comment' => 'Settings set on Privacy tab disabled these extensions',
+
+ ],
+
+ 'mapping' => [
+
+ 'hint' => [
+ 'title' => 'Why fields mapping?',
+ 'content' => '
+ You can build a custom form with own field names and types.
+ System writes all form data in database, but for quick overview Name, Email and Message columns are visible separately in Messages list.
+ So you have to help system to identify these columns by mapping to your form fields.
+ These mappings are also used for autoreply emails where at least Email field mapping is important.
+ ',
+ ],
+
+ 'warning' => [
+ 'title' => 'Can\'t select your form fields?',
+ 'content' => '
+ If you don\'t see your form fields, click on button Save at the bottom of this page and then reload page (F5 or Ctr+R / Cmd+R).
+ ',
+ ],
+
+ ],
+
+ 'privacy' => [
+ 'disable_messages_saving' => 'Disable messages saving',
+ 'disable_messages_saving_comment' => 'When checked, no data will saved in Messages list.This will also disable IP protection! ',
+ 'disable_messages_saving_comment_section' => '',
+ ],
+
+ 'tabs' => [
+ 'form' => 'Form',
+ 'buttons' => 'Send button',
+ 'form_fields' => 'Fields',
+ 'mapping' => 'Columns mapping',
+ 'email' => 'Email',
+ 'antispam' => 'Antispam',
+ 'privacy' => 'Privacy',
+ 'ga' => 'Google Analytics',
+ ],
+
+ ],
+
+ 'components' => [
+
+ 'groups' => [
+
+ 'hacks' => 'Hacks',
+ 'override_form' => 'Override form settings',
+ 'override_notifications' => 'Override notification settings',
+ 'override_autoreply' => 'Override autoreply settings',
+ 'override' => 'Override form settings',
+ 'override_redirect' => 'Override redirect settings',
+ 'override_ga' => 'Override Google Analytics settings',
+ ],
+
+ 'properties' => [
+
+ 'form_description' => 'Form description',
+ 'form_description_comment' => 'You can add optional form description, that will be saved with other sent data in the messages list. You can also use {{ :slug }} here.',
+
+ 'disable_fields' => 'Disable fields',
+ 'disable_fields_comment' => 'This will disable listed fields. Add field names separated by pipe (eg. name|message|phone)',
+
+ 'send_btn_label' => 'Send button label',
+ 'send_btn_label_comment' => 'Override send button label',
+
+ 'form_success_msg' => 'Success message',
+ 'form_success_msg_comment' => 'Override success message shown after successful sent',
+
+ 'form_error_msg' => 'Error message',
+ 'form_error_msg_comment' => 'Override error message shown after unsuccessful sent',
+
+ 'disable_notifications' => 'Disable notification',
+ 'disable_notifications_comment' => 'This will disable notification emails (overrides form settings)',
+
+ 'notification_address_to' => 'Address TO',
+ 'notification_address_to_comment' => 'This will override email address where notification email will be sent (if enabled in form settings)',
+
+ 'notification_address_from' => 'Address FROM',
+ 'notification_address_from_comment' => 'This will override email address from where notification email will be sent',
+
+ 'notification_address_from_name' => 'Address FROM name',
+ 'notification_address_from_name_comment' => 'This will override email address name from where notification email will be sent',
+
+ 'notification_template' => 'Notification template',
+ 'notification_template_comment' => 'This will override notification email template (eg. janvince.smallcontactform::mail.notification)',
+
+ 'notification_subject' => 'Notification subject',
+ 'notification_template_comment' => 'Override email subject',
+
+ 'disable_autoreply' => 'Disable notification',
+ 'disable_autoreply_comment' => 'This will disable notification emails (overrides form settings)',
+
+ 'autoreply_address_from' => 'Address FROM',
+ 'autoreply_address_from_comment' => 'This will override email address in autoreply email (if enabled in form settings)',
+
+ 'autoreply_address_from_name' => 'Address (FROM) name',
+ 'autoreply_address_from_name_comment' => 'This will override email address name in autoreply email (if enabled in form settings)',
+
+ 'autoreply_address_replyto' => 'Address REPLY TO',
+ 'autoreply_address_replyto_comment' => 'This will override REPLY TO email address in autoreply email (if enabled in form settings)',
+
+ 'autoreply_template' => 'Autoreply template',
+ 'autoreply_template_comment' => 'This will override autoreply email template (eg. janvince.smallcontactform::mail.autoreply)',
+
+ 'autoreply_subject' => 'Autoreply email subject',
+ 'autoreply_template_comment' => 'Override email subject',
+
+ ]
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/fr/lang.php b/plugins/janvince/smallcontactform/lang/fr/lang.php
new file mode 100644
index 000000000..87ed6f534
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/fr/lang.php
@@ -0,0 +1,426 @@
+ [
+ 'name' => 'Formulaire de contact',
+ 'description' => 'Générateur simple de formulaire de contact',
+ 'category' => 'Petits plugins',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Accéder à la liste des messages',
+ 'access_settings' => 'Gérer les préférences d\'administration',
+ 'delete_messages' => 'Supprimer les messages stockés',
+ 'export_messages' => 'Exporter des messages',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Formulaire de contact',
+ 'messages' => 'Messages',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Formulaire de contact',
+ 'description' => 'Insérer un formulaire de contact sur la page',
+ 'no_fields' => 'Veuillez ajouter d\’abord des champs de formulaire dans l\’administration dorsale (dans Paramètres > Formulaire de contact > Champs) ...',
+ ],
+
+ 'filter' => [
+ 'date' => 'Plage de dates',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Messages',
+ 'latest_record' => 'Dernier message de',
+ 'new_count' => 'Nouveau',
+ 'new_description' => 'Messages',
+ 'read_count' => 'Lus',
+ 'all_count' => 'Total',
+ 'all_description' => 'Messages',
+ 'settings_btn' => 'Paramètres du formulaire',
+ 'mark_read' => 'Marquer comme lu',
+ 'mark_read_confirm' => 'Voulez-vous vraiment définir les messages sélectionnés comme lus?',
+ 'mark_read_success' => 'Les messages ont été marqués comme lu avec succès.',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Message non trouvé!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Date et heure',
+ 'form_data' => 'Données du formulaire',
+ 'name' => 'Nom',
+ 'email' => 'E-mail',
+ 'message' => 'Message',
+ 'new_message' => 'Statut',
+ 'new' => 'Nouveau',
+ 'read' => 'Lu',
+ 'remote_ip' => 'IP de l\'expéditeur',
+ 'form_alias' => 'Alias',
+ 'form_description' => 'Description',
+ 'created_at' => 'Créé à',
+ 'updated_at' => 'Modifié à',
+ ]
+
+ ],
+
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+
+ 'list_title' => 'Messages',
+ 'preview' => 'Aperçu',
+ 'preview_title' => 'Message du formulaire de contact',
+ 'preview_date' => 'Date : ',
+ 'preview_content_title' => 'Contenu :',
+ 'remote_ip' => 'Envoyé depuis l\'IP :',
+ 'export' => 'Exporter',
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Accès non autorisé',
+ ],
+
+ ],
+
+ 'mail' => [
+
+ 'templates' => [
+
+ 'autoreply' => 'Réponse automatique du formulaire (français)',
+ 'autoreply_cs' => 'Réponse automatique du formulaire (tchèque)',
+
+ 'notification' => 'Message de notification du formulaire (français)',
+ 'notification_cs' => 'Message de notification du formulaire (tchèque)',
+
+ ]
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+
+ 'messages' => [
+ 'label' => 'Formulaire de contact - Statistiques des messages',
+ 'title' => 'Statistiques des messages',
+ 'messages_all' => 'Tout',
+ 'messages_new' => 'Nouveaux',
+ 'messages_read' => 'Lus',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Formulaire de contact - Nouveaux messages',
+ 'title' => 'Nouveaux messages',
+ 'link_text' => 'Cliquez pour afficher la liste des messages',
+ ],
+
+ ],
+
+ ],
+
+ 'settings' => [
+
+ 'form' => [
+
+ 'css_class' => 'Classe CSS du formulaire',
+
+ 'use_placeholders' => 'Utiliser des "placeholder"',
+ 'use_placeholders_comment' => 'Des "placeholder" seront affichés à la plage des étiquettes de champs.',
+
+ 'disable_browser_validation' => 'Désactiver la validation du navigateur',
+ 'disable_browser_validation_comment' => 'Ne pas autoriser la validation intégrée du navigateur et ses fenêtres contextuelles.',
+
+ 'success_msg' => 'Message de réussite du formulaire',
+ 'success_msg_placeholder' => 'Vos données ont été envoyées.',
+
+ 'error_msg' => 'Message d\'erreur du formulaire',
+ 'error_msg_placeholder' => 'Une erreur s\'est produite lors de l\'envoi de vos données!',
+
+ 'allow_ajax' => 'Activer AJAX',
+ 'allow_ajax_comment' => 'Autoriser AJAX avec rechange pour les navigateurs sans JavaScript.',
+
+ 'allow_confirm_msg' => 'Demander une confirmation avant l\'envoi du formulaire',
+ 'allow_confirm_msg_comment' => 'Ajouter une boîte de dialogue de confirmation avant l\'envoi',
+
+ 'send_confirm_msg' => 'Texte de confirmation',
+ 'send_confirm_msg_placeholder' => 'Êtes-vous sûr?',
+
+ 'hide_after_success' => 'Masquer le formulaire après l\'envoi réussi',
+ 'hide_after_success_comment' => 'Afficher uniquement le message de réussite sans formulaire',
+
+ 'add_assets' => 'Ajouter des assets',
+ 'add_assets_comment' => 'Ajouter automatiquement les assets CSS et JS nécessaires (plus d\'informations sur les assets dans le fichier README.md - en anglais)',
+
+ 'add_css_assets' => 'Ajouter des assets CSS',
+ 'add_css_assets_comment' => 'Tous les styles nécessaires seront inclus',
+
+ 'add_js_assets' => 'Ajouter des assets JavaScript',
+ 'add_js_assets_comment' => 'Tous les scripts JavaScript nécessaires seront inclus',
+
+
+ ],
+
+ 'buttons' => [
+ 'send_btn_text' => 'Texte du bouton envoyer',
+ 'send_btn_text_placeholder' => 'Envoyer',
+
+ 'send_btn_css_class' => 'Classe(s) CSS du bouton envoyer',
+ 'send_btn_css_class_placeholder' => 'btn btn-primary',
+
+ 'send_btn_wrapper_css' => 'Classe(s) CSS du wrapper du bouton envoyer',
+ 'send_btn_wrapper_css_placeholder' => 'form-group',
+
+ ],
+
+ 'redirect' => [
+
+ 'allow_redirect' => 'Rediriger après soumission',
+ 'allow_redirect_comment' => 'Rediriger vers une autre page après un envoie réussie',
+
+ 'redirect_url' => 'URL de la page vers laquelle rediriger',
+ 'redirect_url_comment' => 'Entrez l\'URL de votre page (par exemple, /contact/thank-you)',
+ 'redirect_url_placeholder' => '/contact/thank-you',
+
+ 'redirect_url_external' => 'URL externe',
+ 'redirect_url_external_comment' => 'Ceci est un chemin d\'URL externe (ex. http://www.domain.com)',
+
+ ],
+
+ 'form_fields' => [
+ 'prompt' => 'Ajouter un nouveau champ de formulaire',
+
+ 'name' => 'Nom du champ',
+ 'name_comment' => 'Minuscules sans caractères spéciaux (ex. nom, email, adresse_personnelle, ...)',
+
+ 'type' => 'Type de champ',
+
+ 'label' => 'Étiquette',
+ 'label_placeholder' => 'Nom complet',
+
+ 'field_styling' => 'Classe CSS personnalisée',
+ 'field_styling_comment' => 'Changer les styles de Bootstrap par défaut',
+
+ 'autofocus' => 'Champ autofocus',
+ 'autofocus_comment' => 'Autofocus ce champ du formulaire',
+
+ 'wrapper_css' => 'Classe CSS du wrapper',
+ 'wrapper_css_placeholder' => 'form-group',
+
+ 'field_css' => 'Classe CSS du champ',
+ 'field_css_placeholder' => 'form-control',
+
+ 'label_css' => 'Classe CSS de l\'étiquette',
+ 'label_css_placeholder' => '',
+
+ 'field_validation' => 'Validation du champ',
+ 'field_validation_comment' => 'Ajouter des règles de validation du champ',
+
+ 'validation' => 'Validation',
+ 'validation_prompt' => 'Ajouter une validation',
+
+ 'validation_type' => 'Règle de validation',
+
+ 'validation_error' => 'Message d\'erreur de validation',
+ 'validation_error_placeholder' => 'S\'il vous plaît entrer des données valides.',
+ 'validation_error_comment' => 'Message d\'erreur à utiliser lorsque la validation échoue',
+
+ 'custom' => 'Champ personnalisé',
+ 'custom_description' => 'Champ personnalisé avec option de validation',
+
+
+ ],
+
+ 'form_field_types' => [
+ 'text' => 'Texte',
+ 'email' => 'Email',
+ 'textarea' => 'Aire de texte',
+ 'checkbox' => 'Case à cocher',
+ 'dropdown' => 'Dropdown',
+ 'file' => 'File',
+ 'custom_code' => 'Custom code',
+ 'custom_content' => 'Custom content',
+ ],
+
+ 'form_field_validation' => [
+ 'select' => '--- Sélectionnez la validation ---',
+ 'required' => 'Requis',
+ 'email' => 'Email',
+ 'numeric' => 'Numérique',
+ ],
+
+ 'email' => [
+ 'address_from' => 'Adresse de l\'expéditeur',
+ 'address_from_placeholder' => 'john.doe@domain.com',
+
+ 'address_from_name' => 'Nom de l\'expéditeur',
+ 'address_from_name_placeholder' => 'John Doe',
+
+ 'subject' => 'Sujet de l\'e-mail',
+ 'subject_comment' => 'Définissez uniquement si vous souhaitez une définition autre que celle définie dans Paramètres > Modèles des e-mails.',
+
+ 'template' => 'Modèle e-mail',
+ 'template_comment' => 'Code du modèle e-mail créé dans Paramètres > Modèles des e-mails. Laissez vide pour le modèle par défaut: janvince.smallcontactform::mail.autoreply.',
+
+ 'allow_email_queue' => 'E-mail en fille d\'attente',
+ 'allow_email_queue_comment' => 'Ajouter un email à la file d\'attente au lieu de l\'envoyer immédiatement. Vous devez d\'abord configurer votre file d\'attente OctoberCMS!',
+
+ 'allow_notifications' => 'Autoriser les notifications',
+ 'allow_notifications_comment' => 'Envoyer une notification après l\'envoi du formulaire',
+
+ 'notification_address_to' => 'Envoyer une notification à l\'e-mail',
+ 'notification_address_to_comment' => 'Une adresse électronique ou une liste d\'adresses séparées par des virgules',
+ 'notification_address_to_placeholder' => 'notifications@domain.com',
+
+ 'notification_address_from_form' => 'Forcer l\'adresse e-mail de la notification (NON PRIS EN CHARGE par tous les systèmes de messagerie!)',
+ 'notification_address_from_form_comment' => 'Définir l\'adresse e-mail entré dans le formulaire de contact comme adresse de l\'expéditeur (le champ doit être défini dans le mappage de colonnes).',
+
+ 'allow_autoreply' => 'Autoriser une copie à l\'auteur',
+ 'allow_autoreply_comment' => 'Envoyer une copie du contenu du formulaire à l\'auteur',
+
+ 'autoreply_name_field' => 'Champ du formulaire avec le NOM',
+ 'autoreply_name_field_empty_option' => '-- Sélectionnez --',
+ 'autoreply_name_field_comment' => 'Doit être du type Texte.Enregistrez et actualisez cette page si vous ne pouvez pas voir vos champs. ',
+
+ 'autoreply_email_field' => 'Champ du formulaire avec l\'adresse E-MAIL',
+ 'autoreply_email_field_empty_option' => '-- Sélectionnez --',
+ 'autoreply_email_field_comment' => 'Doit être du type email.Enregistrez et actualisez cette page si vous ne pouvez pas voir vos champs. ',
+
+ 'autoreply_message_field' => 'Champ du formulaire avec le contenu du MESSAGE',
+ 'autoreply_message_field_empty_option' => '-- Sélectionnez --',
+ 'autoreply_message_field_comment' => 'Doit être du type Aire de texte ou Texte.Enregistrez et actualisez cette page si vous ne pouvez pas voir vos champs. ',
+
+ 'notification_template' => 'Modèle e-mail de la notification',
+ 'notification_template_comment' => 'Code du modèle de courrier électronique créé dans Paramètres > Modèless des e-mails. Laisser vide pour le modèle par défaut : janvince.smallcontactform::mail.autoreply.',
+
+ ],
+
+ 'antispam' => [
+ 'add_antispam' => 'Ajouter une protection antispam passive',
+ 'add_antispam_comment' => 'Ajouter un contrôle antispam passif simple mais efficace (plus d’informations dans le fichier README.md - en anglais)',
+
+ 'antispam_delay' => 'Délai Antispam (en secondes)',
+ 'antispam_delay_comment' => 'Protection différée pour l\'envoi de formulaires trop rapide (généralement par des robots)',
+ 'antispam_delay_placeholder' => '3',
+
+ 'antispam_label' => 'Étiquette de champ antispam',
+ 'antispam_label_comment' => 'L\'étiquette sera visible pour les navigateurs non activés par JavaScript',
+ 'antispam_label_placeholder' => 'Veuillez effacer ce champ',
+
+ 'antispam_error_msg' => 'Message d\'erreur',
+ 'antispam_error_msg_comment' => 'Message à afficher à l\'utilisateur lorsque la protection antispam est déclenchée',
+ 'antispam_error_msg_placeholder' => 'Veuillez vider ce champ!',
+
+ 'antispam_delay_error_msg' => 'Message d\'erreur de délai',
+ 'antispam_delay_error_msg_comment' => 'Message à montrer à l\'utilisateur lorsque le formulaire a été envoyé trop rapidement',
+ 'antispam_delay_error_msg_placeholder' => 'Formulaire envoyé trop vite! S\'il vous plaît attendre quelques secondes et essayez à nouveau!',
+
+ 'add_google_recaptcha' => 'Ajouter Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Ajoutez reCaptcha au formulaire de contact (plus d\’informations dans le fichier README.md - en anglais). Vous pouvez obtenir les clés d’API sur le Google reCaptcha .',
+
+ 'google_recaptcha_site_key' => 'Clé du site',
+ 'google_recaptcha_site_key_comment' => 'Mettez votre clé du site',
+
+ 'google_recaptcha_secret_key' => 'Clé secrète',
+ 'google_recaptcha_secret_key_comment' => 'Mettez votre clé secrète',
+
+ 'google_recaptcha_error_msg' => 'Message d\'erreur',
+ 'google_recaptcha_error_msg_comment' => 'Message à afficher à l\'utilisateur lorsque le reCAPTCHA n\'est pas validé.',
+ 'google_recaptcha_error_msg_placeholder' => 'Erreur de validation du reCAPTCHA de Google!',
+
+ 'google_recaptcha_scripts_allow' => 'Ajouter automatiquement les scripts JS nécessaires',
+ 'google_recaptcha_scripts_allow_comment' => 'Cela ajoutera un lien vers les scripts JS sur votre site.',
+
+ 'google_recaptcha_locale_allow' => 'Autoriser la détection des paramètres de langues',
+ 'google_recaptcha_locale_allow_comment' => 'Cela ajoutera les paramètres de langues actuels de la page Web au script reCAPTCHA, de sorte qu\'il sera traduit.',
+
+ 'add_ip_protection' => 'Vérifier l\'adresse IP de l\'expéditeur',
+ 'add_ip_protection_comment' => 'Ne pas autoriser trop de formulaires soumis à partir d\'une même adresse IP',
+
+ 'add_ip_protection_count' => 'Formulaire maximum soumis au cours d\'une journée',
+ 'add_ip_protection_count_comment' => 'Nombre de soumissions autorisées à partir d\'une même adresse IP au cours d\'une seule journée',
+ 'add_ip_protection_count_placeholder' => '3',
+
+ 'add_ip_protection_error_get_ip' => 'Nous n\'avons pas pu déterminer votre adresse IP!',
+
+ 'add_ip_protection_error_too_many_submits' => 'Message d\'erreur lorsque trop de messages sont envoyés',
+ 'add_ip_protection_error_too_many_submits_comment' => 'Message d\'erreur à montrer à l\'utilisateur',
+ 'add_ip_protection_error_too_many_submits_placeholder' => 'Trop de messages ont été envoyés par votre adresse aujourd\'hui!',
+
+ 'disabled_extensions' => 'Extensions désactivées',
+ 'disabled_extensions_comment' => 'Les paramètres définis dans l\'onglet Confidentialité ont désactivé ces extensions.',
+
+ ],
+
+ 'mapping' => [
+
+ 'hint' => [
+ 'title' => 'Pourquoi le mappage des champs?',
+ 'content' => '
+ Vous pouvez créer un formulaire personnalisé avec vos propres noms et types de champs.
+ Le système écrit toutes les données de formulaire dans la base de données, mais pour une présentation rapide, les colonnes Nom, E-mail et Message sont visibles séparément dans la listedes messages.
+ Vous devez donc aider le système à identifier ces colonnes en les associant à vos champs de formulaire.
+ Ces mappages sont également utilisés pour les e-mails à réponse automatique dans lesquels au moins le mappage du champ e-mail est important.
+ ',
+ ],
+
+ 'warning' => [
+ 'title' => 'Vous ne pouvez pas sélectionner vos champs de formulaire?',
+ 'content' => '
+ Si vous ne voyez pas vos champs de formulaire, cliquez sur le bouton Enregistrer en bas de cette page, puis rechargez la page (F5 ou Ctrl+R / Cmd+R).
+ ',
+ ],
+
+ ],
+
+ 'privacy' => [
+ 'disable_messages_saving' => 'Désactiver l\'enregistrement des messages',
+ 'disable_messages_saving_comment' => 'Lorsque cette case est cochée, aucune donnée ne sera enregistrée dans la liste de messages.La protection IP sera également désactivée! ',
+ 'disable_messages_saving_comment_section' => '',
+ ],
+
+ 'tabs' => [
+ 'form' => 'Formulaire',
+ 'buttons' => 'Bouton envoyer',
+ 'form_fields' => 'Champs',
+ 'mapping' => 'Mappage des colonnes',
+ 'email' => 'E-mail',
+ 'antispam' => 'Antispam',
+ 'privacy' => 'Confidentialité'
+ ],
+
+ ],
+
+ 'components' => [
+
+ 'groups' => [
+
+ 'hacks' => 'Hacks',
+
+ ],
+ 'properties' => [
+
+ 'disable_notifications' => 'Désactiver les e-mails de notification',
+ 'disable_notifications_comment' => 'Ceci désactivera les courriels de notification (remplace les paramètres du formulaire)',
+
+ 'form_description' => 'Description du formulaire',
+ 'form_description_comment' => 'Vous pouvez ajouter une description de formulaire facultative, qui sera enregistrée avec les autres données envoyées dans la liste de messages. Vous pouvez également utiliser {{:slug}} ici.',
+
+ ]
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/hu/lang.php b/plugins/janvince/smallcontactform/lang/hu/lang.php
new file mode 100644
index 000000000..1275a6bae
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/hu/lang.php
@@ -0,0 +1,146 @@
+ [
+ 'name' => 'Kapcsolat űrlap',
+ 'description' => 'Űrlap generálása kapcsolat felvételhez.',
+ 'category' => 'Small plugins',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Üzenetek megtekintése',
+ 'access_settings' => 'Beállítások módosítása',
+ 'delete_messages' => 'Üzenetek törlése',
+ 'export_messages' => 'Üzenetek exportálása',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Kapcsolat űrlap',
+ 'messages' => 'Üzenetek',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Kapcsolat űrlap',
+ 'description' => 'Űrlap generálása kapcsolat felvételhez.',
+ 'no_fields' => 'Elsőként hozzon létre legalább egy mezőt itt: Beállítások > Small plugins > Kapcsolat űrlap > Fields',
+ ],
+
+ 'filter' => [
+ 'date' => 'Időintervallum',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Üzenetek',
+ 'latest_record' => 'Legutóbbi',
+ 'new_count' => 'Új levél',
+ 'new_description' => 'üzenet',
+ 'read_count' => 'Olvasott',
+ 'all_count' => 'Összes',
+ 'all_description' => 'üzenet',
+ 'settings_btn' => 'Testreszabás',
+ 'mark_read' => 'Olvasottnak jelöl',
+ 'mark_read_confirm' => 'Valóban olvasottnak szeretné jelölni az üzeneteket?',
+ 'mark_read_success' => 'Az üzenetek sikeresen olvasottnak lettek jelölve.',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Üzenet nem található!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Elküldve',
+ 'form_data' => 'Adatok',
+ 'name' => 'Név',
+ 'email' => 'Email',
+ 'message' => 'Üzenet',
+ 'new_message' => 'Státusz',
+ 'new' => 'Új levél',
+ 'read' => 'Olvasott',
+ 'remote_ip' => 'IP cím',
+ 'form_alias' => 'Űrlap',
+ 'form_description' => 'Leírás',
+ 'created_at' => 'Létrehozva',
+ 'updated_at' => 'Módosítva',
+ ],
+
+ ],
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+ 'list_title' => 'Üzenetek',
+ 'preview' => 'Előnézet',
+ 'preview_title' => 'Üzenet',
+ 'preview_date' => 'Elküldve:',
+ 'preview_content_title' => 'Tartalom:',
+ 'remote_ip' => 'IP cím',
+ 'export' => 'Exportálás',
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Illetéktelen hozzáférés',
+ ],
+
+ ],
+
+ 'mail' => [
+
+ 'templates' => [
+
+ 'autoreply' => 'Automatikus üzenet (angol)',
+ 'autoreply_cs' => 'Automatikus üzenet (cseh)',
+
+ 'notification' => 'Értesítő üzenet (angol)',
+ 'notification_cs' => 'Értesítő üzenet (cseh)',
+ ]
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+ 'messages' => [
+ 'label' => 'Kapcsolat űrlap - Statisztika',
+ 'title' => 'Üzenet statisztika',
+ 'messages_all' => 'Összes',
+ 'messages_new' => 'Új levél',
+ 'messages_read' => 'Olvasott',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Kapcsolat űrlap - Új üzenetek',
+ 'title' => 'Új üzenetek',
+ 'link_text' => 'Összes üzenet megtekintése',
+ ],
+
+ ],
+
+ ],
+
+ 'components' => [
+
+ 'groups' => [
+ 'hacks' => 'Hackelés',
+ ],
+
+ 'properties' => [
+ 'disable_notifications' => 'Értesítő e-mailek letiltása',
+ 'disable_notifications_comment' => 'Felül fogja írni az űrlap központi beállítását erre vonatkozóan.',
+ 'form_description' => 'Űrlap leírás',
+ 'form_description_comment' => 'Amennyiben megad adatot, azok mentésre kerülnek az űrlap beküldésekor. Például az aktuális oldal címének megjegyzéséhez használhatja a {{ :slug }} kódot.',
+ ],
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/pl/janvince/smallcontactform/lang.php b/plugins/janvince/smallcontactform/lang/pl/janvince/smallcontactform/lang.php
new file mode 100644
index 000000000..78b522210
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/pl/janvince/smallcontactform/lang.php
@@ -0,0 +1,436 @@
+ [
+ 'name' => 'Formularz kontaktowy',
+ 'description' => 'Kreator prostego formularza kontaktowego',
+ 'category' => 'Small plugins',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Przeglądaj wiadomości',
+ 'access_settings' => 'Zarządzaj ustawieniami',
+ 'delete_messages' => 'Usuwaj zapisane wiadomości',
+ 'export_messages' => 'Eksportuj wiadomości',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Formularz kontaktowy',
+ 'messages' => 'Wiadomości',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Formularz kontaktowy',
+ 'description' => 'Dodaj formularz kontaktowy do strony',
+ 'no_fields' => 'Najpierw dodaj pola formularza w panelu administracyjnym (Ustawienia -> Small plugins -> Formularz kontktowy)...',
+ ],
+
+ 'filter' => [
+ 'date' => 'Zakres dat',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Wiadomości',
+ 'latest_record' => 'Ostatnia wiadomość od',
+ 'new_count' => 'Nowe',
+ 'new_description' => 'Wiadomości',
+ 'read_count' => 'Przeczytane',
+ 'all_count' => 'Wszystkie',
+ 'all_description' => 'Wiadomości',
+ 'settings_btn' => 'Ustawienia formularza',
+ 'mark_read' => 'Oznacz jako przeczytane',
+ 'mark_read_confirm' => 'Czy na pewno oznaczyć wybrane wiadomości jako przeczytane?',
+ 'mark_read_success' => 'Oznaczono jako przeczytane.',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Nie znaleziono wiadomości!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Data i godzina',
+ 'form_data' => 'Dane formularza',
+ 'name' => 'Imię i nazwisko',
+ 'email' => 'Email',
+ 'message' => 'Wiadomość',
+ 'new_message' => 'Status',
+ 'new' => 'Nowa',
+ 'read' => 'Przeczytana',
+ 'remote_ip' => 'Adres IP wysyłającego',
+ 'form_alias' => 'Alias',
+ 'form_description' => 'Opis',
+ 'created_at' => 'Utworzono',
+ 'updated_at' => 'Zaktualizowano',
+ ]
+
+ ],
+
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+
+ 'list_title' => 'Wiadomości',
+ 'preview' => 'Podgląd',
+ 'preview_title' => 'Wiadomość z formularza kontaktowego',
+ 'preview_date' => 'Data otrzymania:',
+ 'preview_content_title' => 'Zawartość:',
+ 'remote_ip' => 'Wysłano z adresu IP',
+ 'export' => 'Eksportuj',
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Nie masz uprawnień aby wyświetlić tą stroną',
+ ],
+
+ ],
+
+ 'mail' => [
+
+ 'templates' => [
+
+ 'autoreply' => 'Wiadomość automatycznej odpowiedzi (Angielski)',
+ 'autoreply_cs' => 'Wiadomość automatycznej odpowiedzi (Czeski)',
+
+ 'notification' => 'Wiadomość powiadamiająca (Angielski)',
+ 'notification_cs' => 'Wiadomość powiadamiająca (Czeski)',
+
+ ]
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+
+ 'messages' => [
+ 'label' => 'Formularz kontaktowy - Statystyki wiadomości',
+ 'title' => 'Statystyki wiadomości',
+ 'messages_all' => 'Wszystkie',
+ 'messages_new' => 'Nowe',
+ 'messages_read' => 'Przeczytane',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Formularz Kontaktowy - Nowe wiadomości',
+ 'title' => 'Nowe wiadomości',
+ 'link_text' => 'Kliknij, aby pokazać listę wiadomości',
+ ],
+
+ ],
+
+ ],
+
+ 'settings' => [
+
+ 'form' => [
+ 'css_class' => 'Klasa CSS formularza',
+
+ 'use_placeholders' => 'Użyj tekstów zastępczych (Placeholder)',
+ 'use_placeholders_comment' => 'Teksty zastępcze będą wyświetlane zamiast etykiet pól. (Etykiety otrzymają styl display: none)',
+
+ 'disable_browser_validation' => 'Wyłącz domyślną walidację przeglądarki',
+ 'disable_browser_validation_comment' => 'Nie zezwalaj na wbudowane w przeglądarkę sprawdzanie poprawności i wyskakujące okienka.',
+
+ 'success_msg' => 'Wiadomość o poprawnym wysłaniu formularza',
+ 'success_msg_placeholder' => 'Twoja wiadomość została wysłana.',
+
+ 'error_msg' => 'Wiadomość o niepoprawnym wysłaniu formularza',
+ 'error_msg_placeholder' => 'Wystąpił błąd podczas wysyłania Twojej wiadomości!',
+
+ 'allow_ajax' => 'Włącz AJAX',
+ 'allow_ajax_comment' => 'Zezwól na asynchroniczny formularz AJAX razem ze wsparciem dla przeglądarek nieobsługujących JavaScript.',
+
+ 'allow_confirm_msg' => 'Zapytaj o potwierdzenie przed wysłaniem formularza',
+ 'allow_confirm_msg_comment' => 'Dodaje okno dialogowe potwierdzenia przed wysłaniem',
+
+ 'send_confirm_msg' => 'Tekst potwierdzenia',
+ 'send_confirm_msg_placeholder' => 'Czy jesteś pewny?',
+
+ 'hide_after_success' => 'Ukryj formularz po pomyślnym wysłaniu',
+ 'hide_after_success_comment' => 'Pokaż tylko wiadomość o pomyślnym wysłaniu wiadomości bez formularza',
+
+ 'add_assets' => 'Dodaj pliki',
+ 'add_assets_comment' => 'Automatycznie dodaje niezbędne pliki CSS i JS (więcej na ten temat w pliku README.md)',
+
+ 'add_css_assets' => 'Dodaj pliki CSS',
+ 'add_css_assets_comment' => 'Dodaje wszystkie niezbędne pliki CSS',
+
+ 'add_js_assets' => 'Dodaj pliki JS',
+ 'add_js_assets_comment' => 'Dodaje wszystkie niezbędne pliki JS',
+ ],
+
+ 'buttons' => [
+ 'send_btn_text' => 'Tekst przycisku wyślij',
+ 'send_btn_text_placeholder' => 'Wyślij',
+
+ 'send_btn_css_class' => 'Klasa CSS przycisku wyślij',
+ 'send_btn_css_class_placeholder' => 'btn btn-primary',
+
+ 'send_btn_wrapper_css' => 'Klasa CSS kontenera przycisku wyślij',
+ 'send_btn_wrapper_css_placeholder' => 'form-group',
+ ],
+
+ 'redirect' => [
+
+ 'allow_redirect' => 'Przekieruj po wysłaniu',
+ 'allow_redirect_comment' => 'Przekierowuje na inną stronę po wysłaniu formularza',
+
+ 'redirect_url' => 'Adres URL strony przekierowania',
+ 'redirect_url_comment' => 'Wprowadź adres URL strony (np. /contact/thank-you)',
+ 'redirect_url_placeholder' => '/contact/thank-you',
+
+ 'redirect_url_external' => 'Zewnętrzny adres URL',
+ 'redirect_url_external_comment' => 'Ten adres URL wskazuje na zewnętrzną domenę (np. http://www.domain.com)',
+
+ ],
+
+ 'form_fields' => [
+ 'prompt' => 'Dodaj nowe pole formularza',
+
+ 'name' => 'NAZWA POLA',
+ 'name_comment' => 'Małe litery bez znaków specjalnych (np. name, email, home_address, ...)',
+
+ 'type' => 'Rodzaj pola',
+
+ 'label' => 'Etykieta',
+ 'label_placeholder' => 'Imię i nazwisko',
+
+ 'field_styling' => 'Niestandardowa klasa CSS',
+ 'field_styling_comment' => 'Zmień domyślne style (Bootstrap)',
+
+ 'autofocus' => 'Automatyczny fokus',
+ 'autofocus_comment' => 'Automatycznie ustaw fokus na to pole',
+
+ 'wrapper_css' => 'Klasa CSS kontenera',
+ 'wrapper_css_placeholder' => 'form-group',
+
+ 'field_css' => 'Klasa CSS pola',
+ 'field_css_placeholder' => 'form-control',
+
+ 'label_css' => 'Klasa CSS etykiety',
+ 'label_css_placeholder' => 'control-label',
+
+ 'field_validation' => 'Walidacja',
+ 'field_validation_comment' => 'Dodaj reguły walidacji',
+
+ 'validation' => 'Walidacja',
+ 'validation_prompt' => 'Dodaj regułę walidacji',
+
+ 'validation_type' => 'Reguła walidacji',
+
+ 'validation_error' => 'Komunikat błędu walidacji',
+ 'validation_error_placeholder' => 'Wprowadź poprawne dane.',
+ 'validation_error_comment' => 'Komunikat wyświetlany, gdy pole nie przejdzie walidacji',
+
+ 'validation_custom_type' => 'Validation rule name',
+ 'validation_custom_type_comment' => 'Wprowadź nazwę reguły walidatora (eg. regex, boolean, ...). See reguły walidacji .',
+ 'validation_custom_type_placeholder' => 'regex',
+
+ 'validation_custom_pattern' => 'Wzorzec reguły walidacji',
+ 'validation_custom_pattern_comment' => 'Pozostaw puste lub wprowadź niestandardowy wzorzec reguły (prawa część reguły znajdująca się po dwukropku - np. [abc] dla reguły regex)',
+ 'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
+
+ 'custom' => 'Pole niestandardowe',
+ 'custom_description' => 'Niestandardowe pole z opcją walidacji',
+
+ 'add_values_prompt' => 'Dodaj opcje',
+ 'field_value_id' => 'Identyfikator opcji',
+ 'field_value_content' => 'Tekst opcji',
+
+ ],
+
+ 'form_field_types' => [
+ 'text' => 'Tekst',
+ 'email' => 'Email',
+ 'textarea' => 'Pole tekstowy',
+ 'checkbox' => 'Pole wyboru',
+ 'dropdown' => 'Lista rozwijalna',
+ 'file' => 'File',
+ 'custom_code' => 'Custom code',
+ 'custom_content' => 'Custom content',
+ ],
+
+ 'form_field_validation' => [
+ 'select' => '--- Wybierz regułę walidacji ---',
+ 'required' => 'Wymagane',
+ 'email' => 'Email',
+ 'numeric' => 'Numeryczne',
+ 'custom' => 'Reguła niestandardowa',
+ ],
+
+ 'email' => [
+ 'address_from' => 'Z adresu',
+ 'address_from_placeholder' => 'example@domain.com',
+
+ 'address_from_name' => 'Nazwa wyświetlana w polu "Od"',
+ 'address_from_name_placeholder' => 'Jan Nowak',
+
+ 'subject' => 'Temat wiadomości',
+ 'subject_comment' => 'Ustaw tylko wtedy, gdy chcesz aby był inny niż zdefiniowano w Ustawienia > Mail > Szablony wiadomości email',
+
+ 'template' => 'Szablon wiadomości',
+ 'template_comment' => 'Kod szablonu wiadomości email utworzony w Ustawienia > Mail > Szablony wiadomości email. Pozostaw puste dla domyślnego szablonu: janvince.smallcontactform::mail.autoreply.',
+
+ 'allow_email_queue' => 'Kolejkowanie wiadomości',
+ 'allow_email_queue_comment' => 'Dodawaj wiadomości do kolejki zamiast wysyłać je natychmiastowo. Pamiętaj, najpierw musisz skonfigurować kolejkę OctoberCMS!',
+
+ 'allow_notifications' => 'Wysyłaj powiadomienia',
+ 'allow_notifications_comment' => 'Wyślij powiadomienie zaraz po wysłaniu formularza',
+
+ 'notification_address_to' => 'Wyślij powiadomienie na adres/adresy',
+ 'notification_address_to_comment' => 'Jeden adres email, lub lista oddzielona przecinkami',
+ 'notification_address_to_placeholder' => 'notifications@domain.com',
+
+ 'notification_address_from_form' => 'Wymuś zawartość pola "Od" powiadomienia (NIE WSPIERANE przez wszystkie systemy email)',
+ 'notification_address_from_form_comment' => 'Ustaw adres, z którego wysyłane jest powiadomienie na adres email podany w formularzu kontaktowym (pole musi być ustawione w odwzorowaniu pól).',
+
+ 'allow_autoreply' => 'Automatyczna odpowiedź',
+ 'allow_autoreply_comment' => 'Wyślij kopię wiadomości do autora',
+
+ 'autoreply_name_field' => 'Pole NAZWA',
+ 'autoreply_name_field_empty_option' => '-- Wybierz --',
+ 'autoreply_name_field_comment' => 'Musi być typu Tekst.Zapisz i odśwież tą stronę jeśli nie widzisz pól. ',
+
+ 'autoreply_email_field' => 'Pole EMAIL',
+ 'autoreply_email_field_empty_option' => '-- Wybierz --',
+ 'autoreply_email_field_comment' => 'Musi być typu Email.Zapisz i odśwież tą stronę jeśli nie widzisz pól. ',
+
+ 'autoreply_message_field' => 'Pole WIADOMOŚĆ',
+ 'autoreply_message_field_empty_option' => '-- Wybierz --',
+ 'autoreply_message_field_comment' => 'Musi być typu Pole tekstowe lub Tekst.Zapisz i odśwież tą stronę jeśli nie widzisz pól. ',
+
+ 'notification_template' => 'Szablon wiadomości powiadomienia',
+ 'notification_template_comment' => 'Kod szablonu wiadomości email utworzony w Ustawienia > Mail > Szablony wiadomości email. Pozostaw puste dla domyślnego szablonu: janvince.smallcontactform::mail.notification.',
+
+ ],
+
+ 'antispam' => [
+ 'add_antispam' => 'Dodaj pasywną ochronę antyspamową',
+ 'add_antispam_comment' => 'Dodaj prostą, ale skuteczną pasywną kontrolę antyspamową (więcej informacji w pliku README.md)',
+
+ 'antispam_delay' => 'Opóźnienie ochrony antyspamowej (s)',
+ 'antispam_delay_comment' => 'Zabezpieczenia w postaci opóźnienia podczas wysyłania dużej ilości wiadomości (zwykle przez roboty)',
+ 'antispam_delay_placeholder' => '3',
+
+ 'antispam_label' => 'Etykieta pola ochrony antyspamowej',
+ 'antispam_label_comment' => 'Etykieta będzie widoczna tylko dla przeglądarek, które nie obsługują JavaScript',
+ 'antispam_label_placeholder' => 'Wyczyść to pole w celu wysłania wiadomości',
+
+ 'antispam_error_msg' => 'Komunikat o błędzie - ochrona antyspamowa',
+ 'antispam_error_msg_comment' => 'Komunikat, który zostanie wyświetlony, gdy uruchomi się ochrona antyspamowa',
+ 'antispam_error_msg_placeholder' => 'Musisz wyczyścić to pole, aby wysłać wiadomość!',
+
+ 'antispam_delay_error_msg' => 'Komunikat o błędzie - opóźnienie',
+ 'antispam_delay_error_msg_comment' => 'Komunikat, który zostanie wyświetlony, gdy użytkownik wyśle wiadomość zbyt szybko',
+ 'antispam_delay_error_msg_placeholder' => 'Wiadomość wysłana zbyt szybko! Poczekaj chwilę, a następnie spróbuj ponownie.',
+
+ 'add_google_recaptcha' => 'Dodaj Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Dodaj reCaptcha do formularza kontaktowego (więcej informacji w pliku README.md). Możesz uzyskać klucze API na stronie Google reCaptcha .',
+
+ 'google_recaptcha_site_key' => 'Klucz strony',
+ 'google_recaptcha_site_key_comment' => 'Wpisz swój kod strony',
+
+ 'google_recaptcha_secret_key' => 'Sekretny klucz',
+ 'google_recaptcha_secret_key_comment' => 'Wpisz swój sekretny klucz',
+
+ 'google_recaptcha_wrapper_css' => 'Klasa CSS kontenera reCaptcha',
+ 'google_recaptcha_wrapper_css_comment' => 'Klasa CSS kontenera wokół pola reCaptcha',
+ 'google_recaptcha_wrapper_css_placeholder' => 'form-group',
+
+ 'google_recaptcha_error_msg' => 'Komunikat błędu',
+ 'google_recaptcha_error_msg_comment' => 'Komunikat, który pokaże się, gdy reCAPTCHA zwróci błąd.',
+ 'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA validation error!',
+
+ 'google_recaptcha_scripts_allow' => 'Automatycznie dodaj niezbędne skrypt JS',
+ 'google_recaptcha_scripts_allow_comment' => 'Dodaj skrypty JS do twojej strony',
+
+ 'google_recaptcha_locale_allow' => 'Zezwól na wykrywanie ustawień regionalnych',
+ 'google_recaptcha_locale_allow_comment' => 'Spowoduje to dodanie bieżących ustawień regionalnych do skryptu reCAPTCHA, tak że będzie ona przetłumaczona.',
+
+ 'add_ip_protection' => 'Sprawdzaj adres IP wysyłającego',
+ 'add_ip_protection_comment' => 'Nie zezwalaj na zbyt dużo wiadomości z jednego adresu IP',
+
+ 'add_ip_protection_count' => 'Maksymalna liczba wiadomości na dzień',
+ 'add_ip_protection_count_comment' => 'Maksymalna liczba wiadomości możliwa do wysłania z jednego adresu IP na dzień',
+ 'add_ip_protection_count_placeholder' => '3',
+
+ 'add_ip_protection_error_get_ip' => 'Nie udało się ustalić Twojego adresu IP',
+
+ 'add_ip_protection_error_too_many_submits' => 'Komunikat błędu - za dużo wiadomości',
+ 'add_ip_protection_error_too_many_submits_comment' => 'Komunikat, który zostanie pokazany użytkownikowi',
+ 'add_ip_protection_error_too_many_submits_placeholder' => 'Otrzymaliśmy dzisiaj zbyt wiele wiadomości z jednego adresu IP. Spróbuj ponownie jutro.',
+
+ 'disabled_extensions' => 'Wyłączone rozszerzenia',
+ 'disabled_extensions_comment' => 'Ustawienia na zakładce Prywatność wyłączyły te rozszerzenia',
+
+ ],
+
+ 'mapping' => [
+
+ 'hint' => [
+ 'title' => 'Dlaczego odwzorowywanie pól?',
+ 'content' => '
+ Możesz zbudować niestandardowy formularz z własnymi nazwami i typami pól.
+ System zapisuje wszystkie dane formularze w bazie danych. W celu szybkiego podglądu kolumny NAZWA, EMAIL i WIADOMOŚĆ są widoczne osobno na liście wiadomości.
+ Musisz więc pomóc systemowi w identyfikacji tych kolumn poprzez odwzorowanie pól formularza.
+ Odwzorowania są również używane przez system automatycznej odpowiedzi na wiadomości, w których ważne jest przynajmniej pole EMAIL.
+ ',
+ ],
+
+ 'warning' => [
+ 'title' => 'Nie możesz wybrać pól?',
+ 'content' => 'Jeśli nie widzisz pól formularza wciśnij przycisk zapisz na dole strony a następnie odśwież stronę (F5 lub Ctrl + R / Cmd + R)
',
+ ],
+
+ ],
+
+ 'privacy' => [
+ 'disable_messages_saving' => 'Wyłącz zapisywanie wiadomości',
+ 'disable_messages_saving_comment' => 'Żadne dane nie zostaną zapisane w liście wiadomości. To ustawienie wyłączy również ochronę na podstawie adresu IP! ',
+ 'disable_messages_saving_comment_section' => '',
+ ],
+
+ 'tabs' => [
+ 'form' => 'Formularz',
+ 'buttons' => 'Przycisk wyślij',
+ 'form_fields' => 'Pola',
+ 'mapping' => 'Odwzorowanie pól',
+ 'email' => 'Email',
+ 'antispam' => 'Ochrona antyspamowa',
+ 'privacy' => 'Prywatność'
+ ],
+
+ ],
+
+ 'components' => [
+
+ 'groups' => [
+
+ 'hacks' => 'Hacki',
+
+ ],
+ 'properties' => [
+
+ 'disable_notifications' => 'Wyłącz powiadomienia e-mail',
+ 'disable_notifications_comment' => 'Spowoduje wyłączenie powiadomień e-mail (nadpisuje ustawienia globalne)',
+
+ 'form_description' => 'Opis formularza',
+ 'form_description_comment' => 'Możesz dodać opcjonalny opis formularza, który zostanie zapisany wraz z danymi wysłanymi przez formularz. Może także użyć {{ :slug }}.',
+
+ ]
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/ru/lang.php b/plugins/janvince/smallcontactform/lang/ru/lang.php
new file mode 100644
index 000000000..8959431e1
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/ru/lang.php
@@ -0,0 +1,322 @@
+ [
+ 'name' => 'Контактная форма',
+ 'description' => 'Простой конструктор контактной формы',
+ ],
+
+ 'permissions' => [
+ 'access_messages' => 'Доступ к списку сообщений',
+ 'access_settings' => 'Управление настройками бэкенд',
+ 'delete_messages' => 'Удалить сохраненные сообщения',
+ ],
+
+ 'navigation' => [
+ 'main_label' => 'Контакт форма',
+ 'messages' => 'Сообщения',
+ ],
+
+ 'controller' => [
+
+ 'contact_form' => [
+ 'name' => 'Контактная форма',
+ 'description' => 'Вставьте контактную форму на страницу',
+ 'no_fields' => 'Для работы плагина следует добавить поля формы в бэкенд (Настройки > Контактная форма > Поля)...',
+ ],
+
+ 'filter' => [
+ 'date' => 'Диапазон дат',
+ ],
+
+ 'scoreboard' => [
+ 'records_count' => 'Сообщения',
+ 'latest_record' => 'Последнее от',
+ 'new_count' => 'Новые',
+ 'new_description' => 'Сообщения',
+ 'read_count' => 'Прочитанные',
+ 'all_count' => 'Всего',
+ 'all_description' => 'Сообщения',
+ 'settings_btn' => 'Настройки формы',
+ 'mark_read' => 'Отметить как прочитанное',
+ 'mark_read_confirm' => 'Вы действительно хотите отметить выбранные сообщения как прочитанные?',
+ 'mark_read_success' => 'Успешно отмечено как прочитанное',
+ ],
+
+ 'preview' => [
+ 'record_not_found' => 'Сообщение не найдено!',
+ ],
+
+ ],
+
+ 'models' => [
+
+ 'message' => [
+
+ 'columns' => [
+ 'datetime' => 'Дата и время',
+ 'form_data' => 'Данные формы',
+ 'name' => 'Имя',
+ 'message' => 'Сообщение',
+ 'new_message' => 'Статус',
+ 'new' => 'Новое',
+ 'read' => 'Прочитанное',
+ 'remote_ip' => 'IP отправителя',
+ 'form_alias' => 'Псевдоним',
+ 'form_description' => 'Описание',
+ ]
+
+ ],
+
+
+ ],
+
+ 'controllers' => [
+
+ 'messages' => [
+
+ 'list_title' => 'Сообщения',
+ 'preview' => 'Предварительный просмотр',
+ 'preview_title' => 'Сообщение контактной формы',
+ 'preview_date' => 'От даты:',
+ 'preview_content_title' => 'Содержание:',
+ 'remote_ip' => 'Отправлено с IP',
+
+ ],
+
+ 'index' => [
+ 'unauthorized' => 'Несанкционированный доступ',
+ ],
+
+ ],
+
+ 'reportwidget' => [
+
+ 'partials' => [
+
+ 'messages' => [
+ 'label' => 'Контактная форма - Статистика сообщений',
+ 'title' => 'Статистика сообщений',
+ 'messages_all' => 'Все',
+ 'messages_new' => 'Новые',
+ 'messages_read' => 'Прочитанные',
+ ],
+
+ 'new_message' => [
+ 'label' => 'Контактная форма - Новое сообщение',
+ 'title' => 'Новое сообщение',
+ 'link_text' => 'Нажмите, чтобы показать список сообщений',
+ ],
+
+ ],
+
+ ],
+
+ 'settings' => [
+
+ 'form' => [
+
+ 'css_class' => 'CSS классы формы',
+
+ 'use_placeholders' => 'Использовать заполнители формы',
+ 'use_placeholders_comment' => 'Будет скрыт , а текст метки будет показан внутри поля.',
+
+ 'disable_browser_validation' => 'Отключить валидацию браузера',
+ 'disable_browser_validation_comment' => 'Запретить встроенные проверки и всплывающие окна браузера.',
+
+ 'success_msg' => 'Текст сообщения об успешной отправке',
+ 'success_msg_placeholder' => 'Ваше сообщение доставлено',
+
+ 'error_msg' => 'Текст сообщения о ошибке',
+ 'error_msg_placeholder' => 'При отправке сообщения произошла ошибка',
+
+ 'allow_ajax' => 'Включить AJAX',
+ 'allow_ajax_comment' => 'Разрешить AJAX с откатом для браузеров без JavaScript.',
+
+ 'allow_confirm_msg' => 'Запрашивать подтверждение перед отправкой формы',
+ 'allow_confirm_msg_comment' => 'Добавить текст диалога перед отправкой.',
+
+ 'send_confirm_msg' => 'Текст подтверждения',
+ 'send_confirm_msg_placeholder' => 'Отправить сообщение?',
+
+ 'hide_after_success' => 'Скрыть форму после успешной отправки',
+ 'hide_after_success_comment' => 'Показать только сообщение об успешной отправке.',
+
+ ],
+
+ 'buttons' => [
+ 'send_btn_text' => 'Текст на кнопке',
+ 'send_btn_text_placeholder' => 'Отправить',
+
+ 'send_btn_css_class' => 'CSS классы кнопки',
+
+ 'send_btn_wrapper_css' => 'CSS классы обертки (wrapper) кнопки',
+
+ ],
+
+ 'redirect' => [
+
+ 'allow_redirect' => 'Редирект после отправки',
+ 'allow_redirect_comment' => 'Перенаправить пользователя на другую страницу после успешного отправки формы.',
+
+ 'redirect_url' => 'URL страницы для перенаправления',
+ 'redirect_url_comment' => 'Введите URL-адрес страницы (например: /contact/thank-you)',
+
+ 'redirect_url_external' => 'Внешний URL',
+ 'redirect_url_external_comment' => 'Отметьте, если это внешний адрес (например: http://www.domain.com).',
+
+ ],
+
+ 'form_fields' => [
+ 'prompt' => 'Добавить новое поле формы',
+
+ 'name' => 'ИМЯ ПОЛЯ',
+ 'name_comment' => 'Нижний регистр без специальных символов (например: name, email, home_address, ...).',
+
+ 'type' => 'Тип поля',
+
+ 'label' => 'Метка поля',
+ 'label_placeholder' => 'Ваше имя',
+
+ 'field_styling' => 'Пользовательские CSS классы',
+ 'field_styling_comment' => 'Изменение стандартных Bootstrap стилей.',
+
+ 'autofocus' => 'Автофокус',
+ 'autofocus_comment' => 'Автофокусировка на этом поле формы.',
+
+ 'wrapper_css' => 'CSS классы обертки (wrapper)',
+
+ 'field_css' => 'CSS классы полей ',
+
+ 'label_css' => 'CSS классы метки ',
+ 'label_css_placeholder' => '',
+
+ 'field_validation' => 'Валидация поля',
+ 'field_validation_comment' => 'Добавить правила для проверки поля.',
+
+ 'validation' => 'Валидация',
+ 'validation_prompt' => 'Добавить валидацию',
+
+ 'validation_error' => 'Сообщение при ошибке валидации',
+ 'validation_error_placeholder' => 'Данные введены не верно',
+ 'validation_error_comment' => 'Сообщение об ошибке, если валидация не пройдена.',
+
+ ],
+
+ 'email' => [
+ 'address_from' => 'От адреса',
+
+ 'address_from_name' => 'От имени',
+
+ 'subject' => 'Тема письма',
+ 'subject_comment' => 'Установите если хотите использовать отличное от шаблона, Настройки > Шаблоны почты.',
+
+ 'template' => 'Шаблон почты',
+ 'template_comment' => 'Код шаблона почты, созданный в Настройки > Шаблоны почты. Если оставить пустым будет использован по умолчанию: janvince.smallcontactform::mail.autoreply.',
+
+ 'allow_notifications' => 'Разрешить уведомления',
+ 'allow_notifications_comment' => 'Отправлять уведомление на email после отправки формы.',
+
+ 'notification_address_to' => 'Отправить уведомление на email',
+
+ 'allow_autoreply' => 'Автоматический ответ',
+ 'allow_autoreply_comment' => 'Отправлять копию содержимого формы автору.',
+
+ 'autoreply_name_field' => 'Поле формы NAME',
+ 'autoreply_name_field_comment' => 'Должен быть тип поля: Text.Сохраните и обновите эту страницу, если вы не видите свои поля. ',
+
+ 'autoreply_email_field' => 'Поле формы EMAIL',
+ 'autoreply_email_field_comment' => 'Должен быть тип поля: Email.Сохраните и обновите эту страницу, если вы не видите свои поля. ',
+
+ 'autoreply_message_field' => 'Поле формы MESSAGE',
+ 'autoreply_message_field_comment' => 'Должен быть тип поля: Textarea или Text.Сохраните и обновите эту страницу, если вы не видите свои поля. ',
+
+ 'notification_template' => 'Шаблон почты для уведомления',
+ 'notification_template_comment' => 'Код шаблона почты, созданный в Настройки > Шаблоны почты. Если оставить пустым будет использован по умолчанию: janvince.smallcontactform::mail.autoreply.',
+
+ ],
+
+ 'antispam' => [
+ 'add_antispam' => 'Добавить пассивную антиспам защиту',
+ 'add_antispam_comment' => 'Простая, но эффективная антиспам защита (подробнее в README.md файле).',
+
+ 'antispam_delay' => 'Антиспам задержка в секундах',
+ 'antispam_delay_comment' => 'Защищает от слишком быстрой отправки формы (против роботов).',
+ 'antispam_delay_placeholder' => '3',
+
+ 'antispam_label' => 'Метка поля антиспама',
+ 'antispam_label_comment' => 'Будет отображаться в браузерах с выключенным JavaScript.',
+ 'antispam_label_placeholder' => 'Пожалуйста очистите это поле',
+
+ 'antispam_error_msg' => 'Текст сообщения при ошибке',
+ 'antispam_error_msg_comment' => 'Сообщение будет показано пользователю, при срабатывании защиты от спама',
+ 'antispam_error_msg_placeholder' => 'Антиспам - Пожалуйста очистите это поле!',
+
+ 'antispam_delay_error_msg' => 'Текст сообщения при ошибке',
+ 'antispam_delay_error_msg_comment' => 'Сообщение для пользователя, чтобы показать, что форма была отправлена слишком быстро.',
+ 'antispam_delay_error_msg_placeholder' => 'Форма отправлена слишком быстро, пожалуйста подождите 5 секунд.',
+
+ 'add_google_recaptcha' => 'Добавить Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Добавить reCaptcha в контактную форму (подробнее в README.md файле). Вы можете получить API ключ на Google reCaptcha site .',
+
+ 'google_recaptcha_site_key_comment' => 'Введите ключ своего сайта',
+
+ 'google_recaptcha_secret_key_comment' => 'Введите секретный ключ',
+
+ 'google_recaptcha_error_msg' => 'Текст сообщения при ошибке',
+ 'google_recaptcha_error_msg_comment' => 'Сообщение для пользователя, когда он не прошел проверку reCAPTCHA.',
+ 'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA ошибка валидации!',
+
+ 'google_recaptcha_scripts_allow' => 'Автоматический добавить JavaScript',
+ 'google_recaptcha_scripts_allow_comment' => 'Добавить загрузку необходимых JavaScript на ваш сайт.',
+
+ 'google_recaptcha_locale_allow' => 'Разрешить локализацию',
+ 'google_recaptcha_locale_allow_comment' => 'Переведет reCAPTCHA, в соответствии с языком веб-страницы.',
+
+ 'add_ip_protection' => 'Проверять IP-адрес отправителя',
+ 'add_ip_protection_comment' => 'Ограничивает число отправки форм с одного IP-адреса.',
+
+ 'add_ip_protection_count' => 'Максимальное число отправки форм за день',
+ 'add_ip_protection_count_comment' => 'Количество разрешенных отправлений формы с одного IP-адреса в течение одного дня.',
+ 'add_ip_protection_count_placeholder' => '3',
+
+ 'add_ip_protection_error_too_many_submits' => 'Сообщение об ошибке',
+ 'add_ip_protection_error_too_many_submits_comment' => 'Текст ошибки при привышении максимального числа отправок, для показа пользователю.',
+ 'add_ip_protection_error_too_many_submits_placeholder' => 'С вашего IP отправлено слишком много форм!',
+
+
+ ],
+
+ 'mapping' => [
+
+ 'hint' => [
+ 'title' => 'Почему отображение полей?',
+ 'content' => '
+ Вы можете создать пользовательскую форму с собственными именами и типами полей.
+ Система записывает все данные формы в базу данных. Для быстрого обзора в бэкенд Контакт форма > Сообщения, нужно привязать поля: Имя, Email, Сообщения.
+ Поэтому вам необходимо помочь системе идентифицировать эти столбцы, сопоставляя их с полями формы.
+ Эти сопоставления также используются для атоматической отправки писем, где важно, по крайней мере, сопоставление поля Email.
+ ',
+ ],
+
+ 'warning' => [
+ 'title' => 'Не можете выбрать поля формы?',
+ 'content' => '
+ Если вы не видите свои поля, нажмите кнопку «Сохранить» в нижней части этой страницы, а затем перезагрузите страницу (F5 или Ctr+R / Cmd+R).
+ ',
+ ],
+
+ ],
+
+ 'tabs' => [
+ 'form' => 'Форма',
+ 'buttons' => 'Кнопка отправки',
+ 'form_fields' => 'Редактор полей',
+ 'mapping' => 'Отображение',
+ 'email' => 'Email',
+ 'antispam' => 'АнтиСпам',
+ ],
+
+ ],
+
+];
diff --git a/plugins/janvince/smallcontactform/lang/sk/lang.php b/plugins/janvince/smallcontactform/lang/sk/lang.php
new file mode 100644
index 000000000..c22e93da7
--- /dev/null
+++ b/plugins/janvince/smallcontactform/lang/sk/lang.php
@@ -0,0 +1,305 @@
+ [
+ 'name' => 'Kontaktný formulár',
+ 'description' => 'Jednoduchý kontaktný formulár',
+ 'category' => 'Small plugins',
+ ],
+ 'permissions' => [
+ 'access_messages' => 'Prístup k zoznamu správ',
+ 'access_settings' => 'Prístup k nastaveniam',
+ 'delete_messages' => 'Zmazať vybrané správy',
+ 'export_messages' => 'Exportovať správy',
+ ],
+ 'navigation' => [
+ 'main_label' => 'Kontaktný formulár',
+ 'messages' => 'Správy',
+ ],
+ 'controller' => [
+ 'contact_form' => [
+ 'name' => 'Kontaktný formulár',
+ 'description' => 'Pridá na stránku kontaktný formulár',
+ 'no_fields' => 'Pridajte prosím nejaké formulárové polia v administrácií systému (Nastavenia > Kontaktný formulár > Polia)...',
+ ],
+ 'filter' => [
+ 'date' => 'Rozmedzie dátumu',
+ ],
+ 'scoreboard' => [
+ 'records_count' => 'Správy',
+ 'latest_record' => 'najnovšie od',
+ 'new_count' => 'Nové',
+ 'new_description' => 'Správ',
+ 'read_count' => 'Prečítané',
+ 'all_count' => 'Celkom',
+ 'all_description' => 'Správ',
+ 'settings_btn' => 'Nastavenie formulára',
+ 'mark_read' => 'Označiť ako prečítané',
+ 'mark_read_confirm' => 'Naozaj chcete vybrané správy označiť ako prečítané?',
+ 'mark_read_success' => 'Správy boli označené ako prečítané.',
+ ],
+ 'preview' => [
+ 'record_not_found' => 'Správa nebola nájdená!',
+ ],
+ ],
+ 'models' => [
+ 'message' => [
+ 'columns' => [
+ 'id' => 'ID',
+ 'datetime' => 'Dátum a čas',
+ 'form_data' => 'Dáta formulára',
+ 'name' => 'Meno',
+ 'email' => 'Email',
+ 'message' => 'Správa',
+ 'new_message' => 'Stav',
+ 'new' => 'Nová',
+ 'read' => 'Prečítaná',
+ 'remote_ip' => 'IP odosielateľa',
+ 'created_at' => 'Dátum vytvorenia',
+ 'updated_at' => 'Dátum aktualizácie',
+ ]
+ ],
+ ],
+ 'controllers' => [
+ 'messages' => [
+ 'list_title' => 'Správy',
+ 'preview' => 'Náhľad',
+ 'preview_title' => 'Správa z kontaktného formulára',
+ 'preview_date' => 'Z dňa:',
+ 'preview_content_title' => 'Obsah:',
+ 'remote_ip' => 'odoslané z ip',
+ 'form_alias' => 'Alias',
+ 'form_description' => 'Popis',
+ 'export' => 'Export',
+ ],
+ 'index' => [
+ 'unauthorized' => 'Neoprávnený prístup!',
+ ],
+ ],
+ 'mail' => [
+ 'templates' => [
+ 'autoreply' => 'Správa automatickej odpovede z kontaktného formulára (Anglicky)',
+ 'notification' => 'Notifikácia z kontaktného formulára (Anglicky)',
+ 'autoreply_cs' => 'Správa automatickej odpovede z kontaktného formulára (Česky)',
+ 'notification_cs' => 'Notifikácia z kontaktného formulára (Česky)',
+ ]
+ ],
+ 'reportwidget' => [
+ 'partials' => [
+ 'messages' => [
+ 'label' => 'Kontaktný formulár - Prehľad správ',
+ 'title' => 'Prehľad správ',
+ 'messages_all' => 'Všetky',
+ 'messages_new' => 'Nové',
+ 'messages_read' => 'Prečítané',
+ ],
+ 'new_message' => [
+ 'label' => 'Kontaktný formulár - Nové správy',
+ 'title' => 'Nové správy',
+ 'link_text' => 'Kliknite pre zobrazenie prehľadu správ',
+ ],
+ ],
+ ],
+ 'settings' => [
+ 'form' => [
+ 'css_class' => 'CSS trieda formulára',
+ 'use_placeholders' => 'Používať zástupný text (placeholder)',
+ 'use_placeholders_comment' => 'Miesto popiskov nad formulárovými poliami bude použitý zástupný text',
+ 'disable_browser_validation' => 'Zakázať validáciu prehliadačom',
+ 'disable_browser_validation_comment' => 'Nepovoliť prehliadaču použiť vlastnú validáciu a zobrazovať výstrahy.',
+ 'success_msg' => 'Správa po úspešnom odoslaní',
+ 'success_msg_placeholder' => 'Formulár bol v poriadku odoslaný.',
+ 'error_msg' => 'Chybová správa',
+ 'error_msg_placeholder' => 'Pri odosielaní formulára došlo k chybe!',
+ 'allow_ajax' => 'Povoliť AJAX',
+ 'allow_ajax_comment' => 'Povolí AJAX, ale umožní fungovanie formulára aj na prehliadačoch s vypnutým JavaScriptom',
+ 'allow_confirm_msg' => 'Požadovať potvrdenie pred odoslaním',
+ 'allow_confirm_msg_comment' => 'Zobrazí potvrdzovacie okno pred odoslaním formulára',
+ 'send_confirm_msg' => 'Text potvrdenia',
+ 'send_confirm_msg_placeholder' => 'Naozaj chcete odoslať formulár?',
+ 'hide_after_success' => 'Skryť formulár po úspešnom odoslaní',
+ 'hide_after_success_comment' => 'Po odoslaní zobrazí iba správu s potvrdením bez formulára',
+ 'add_assets' => 'Pridať doplnky',
+ 'add_assets_comment' => 'Automaticky vloží potrebné CSS štýly a JS skripty (Viac informácií je v súbore README.md)',
+ 'add_css_assets' => 'Pridať CSS štýly',
+ 'add_css_assets_comment' => 'Vloží všetky potrebné štýly',
+ 'add_js_assets' => 'Pridať JS skripty',
+ 'add_js_assets_comment' => 'Vloží všetky potrebné skripty',
+ ],
+ 'buttons' => [
+ 'send_btn_text' => 'Text odosielacieho tlačidla',
+ 'send_btn_text_placeholder' => 'Odoslať',
+ 'send_btn_css_class' => 'CSS trieda odosielacieho tlačidla',
+ 'send_btn_css_class_placeholder' => 'btn btn-primary',
+ 'send_btn_wrapper_css' => 'CSS trieda kontajneru',
+ 'send_btn_wrapper_css_placeholder' => 'form-group',
+ ],
+ 'redirect' => [
+ 'allow_redirect' => 'Presmerovať po úspěšnom odoslaní',
+ 'allow_redirect_comment' => 'Presmerovať na inú stránku po úspešnom odoslaní formulára',
+ 'redirect_url' => 'URL stránky pre presmerovanie',
+ 'redirect_url_comment' => 'Vložte URL adresu stránky, kam bude presmerované (např. /kontakt/dakujeme)',
+ 'redirect_url_placeholder' => '/kontakt/dakujeme',
+ 'redirect_url_external' => 'Externá URL',
+ 'redirect_url_external_comment' => 'Toto je adresa externej stránky (napr. http://www.domain.com)',
+ ],
+ 'form_fields' => [
+ 'prempt' => 'Pridať nové pole formulára',
+ 'name' => 'NÁZOV POĽA',
+ 'name_comment' => 'Malými písmenami bez diakritiky (napr. meno, email, vasa_poznamka, ...)',
+ 'type' => 'Typ poľa',
+ 'label' => 'Popisok (label)',
+ 'label_placeholder' => 'Pole formulára',
+ 'field_styling' => 'Vlastné CSS triedy',
+ 'field_styling_comment' => 'Môžete pridať vlastné štýly',
+ 'autofocus' => 'Automaticky zvýrazniť (autofocus)',
+ 'autofocus_comment' => 'Po zobrazení nastaviť na pole kurzor',
+ 'wrapper_css' => 'CSS trieda kontajneru',
+ 'wrapper_css_placeholder' => 'form-group',
+ 'field_css' => 'CSS trieda poľa',
+ 'field_css_placeholder' => 'form-control',
+ 'label_css' => 'CSS trieda popisku (label)',
+ 'label_css_placeholder' => '',
+ 'field_validation' => 'Validačné pravidlá poľa',
+ 'field_validation_comment' => 'Povolí nastavenie vlastných validačných pravidiel',
+ 'validation' => 'Pravidlo',
+ 'validation_prempt' => 'Pridať pravidlo',
+ 'validation_type' => 'Typ',
+ 'validation_error' => 'Chybová správa',
+ 'validation_error_placeholder' => 'prosím vložte správne dáta.',
+ 'validation_error_comment' => 'Chybová hláška, ktorá sa zobrazí pri poli',
+ 'custom' => 'vlastné pole',
+ 'custom_description' => 'vlastné pole s validačnými pravidlami',
+ ],
+ 'form_field_types' => [
+ 'text' => 'Text',
+ 'email' => 'Email',
+ 'textarea' => 'Textarea',
+ 'checkbox' => 'Checkbox',
+ 'dropdown' => 'Dropdown',
+ 'file' => 'File',
+ 'custom_code' => 'Custom code',
+ 'custom_content' => 'Custom content',
+ ],
+ 'form_field_validation' => [
+ 'select' => '--- Vyberte pravidlo ---',
+ 'required' => 'Vyžadované',
+ 'email' => 'Email',
+ 'numeric' => 'Číslo',
+ ],
+ 'email' => [
+ 'address_from' => 'Adresa OD',
+ 'address_from_placeholder' => 'john.doe@domain.com',
+ 'address_from_name' => 'Meno odesielateľa',
+ 'address_from_name_placeholder' => 'John Doe',
+ 'subject' => 'Peedmet emailu',
+ 'subject_comment' => 'Nastavte iba pokiaľ chcete prepísať predmet definovaný v šablóne (Nastavenia > E-mailové šablóny).',
+ 'template' => 'Šablóna emailu',
+ 'template_comment' => 'Kód emailovej šablóny vytvorenej v Nastavenia > E-mailové šablóny. Nechajte prázdne pre predvolenú šablónu: janvince.smallcontactform::mail.autoreply.',
+ 'allow_email_queue' => 'Řadit do fronty',
+ 'allow_email_queue_comment' => 'Pridať emaily do fronty místo okamžitého odoslaní. Musíte ale nejdříve správně nakonfigurovat frontu systému OctoberCMS!',
+ 'allow_notifications' => 'Povoliť odosielanie upozornění',
+ 'allow_notifications_comment' => 'Odesílat upozornení, pokiaľ niekto odošle formulár.',
+ 'notification_address_to' => 'Upozornenia posielať na adresu:',
+ 'notification_address_to_comment' => 'Jedna emailová adresa alebo zoznam adries oddelených čiarkami',
+ 'notification_address_to_placeholder' => 'notifications@domain.com',
+ 'notification_address_from_form' => 'nastaviť adresu Od na email z formulára (NEMUSÍ PODPOROVAŤ váš emailový systém!)',
+ 'notification_address_from_form_comment' => 'Nastaví u odosielaného upozornenia adresu Od (From) na tú, ktorá bola zadaná vo formulári (stĺpec email musí mať nastavenú väzbu).',
+ 'allow_autoreply' => 'Povoliť automatickú odpoveď',
+ 'allow_autoreply_comment' => 'Poslať automatickú odpoveď odosielateľovi formulára',
+ 'autoreply_name_field' => 'Pole formulára, ktoré obsahuje MENO odosielateľa',
+ 'autoreply_name_field_empty_option' => '-- Vyberte --',
+ 'autoreply_name_field_comment' => 'Pole typu Text.',
+ 'autoreply_email_field' => 'Pole formulára, ktoré obsahuje EMAIL odosielateľa',
+ 'autoreply_email_field_empty_option' => '-- Vyberte --',
+ 'autoreply_email_field_comment' => 'Pole typu Email.',
+ 'autoreply_message_field' => 'Pole formulára, ktoré obsahuje SPRÁVU',
+ 'autoreply_message_field_empty_option' => '-- vyberte --',
+ 'autoreply_message_field_comment' => 'Pole typu Textarea alebo Text.',
+ 'notification_template' => 'Šablóna notifikačného emailu',
+ 'notification_template_comment' => 'Kód emailovej šablóny vytvorenej v Nastavenia > E-mailové šablóny. Nechajte prázdne pre predvolenú šablónu: janvince.smallcontactform::mail.notification.',
+ ],
+ 'antispam' => [
+ 'add_antispam' => 'Pridať pasívnu ochranu proti spamu',
+ 'add_antispam_comment' => 'Pridá jednoduchú ale efektívnu pasívnu ochranu proti robotom (viac informácií v súbore README.md)',
+ 'antispam_delay' => 'Meškanie formulára (s)',
+ 'antispam_delay_comment' => 'Test na príliš rýchle odoslanie formulára (väčšinou robotmi)',
+ 'antispam_delay_placeholder' => '3',
+ 'antispam_label' => 'Popisok (label) antispamového poľa',
+ 'antispam_label_comment' => 'Popisok bude viditeľný iba na prehliadačoch bez podpory JavaScriptu',
+ 'antispam_label_placeholder' => 'prosím vymažte toto pole',
+ 'antispam_error_msg' => 'Chybová správa',
+ 'antispam_error_msg_comment' => 'Správa, ktorá se zobrazí, pokiaľ sa aktivuje pasívny antispam',
+ 'antispam_error_msg_placeholder' => 'prosím vymažte obsah tohoto poľa!',
+ 'antispam_delay_error_msg' => 'Chybová správa pri rýchlom odoslaní',
+ 'antispam_delay_error_msg_comment' => 'Správa, ktorá se zobrazí pri príliš rýchlom odoslaní formulára',
+ 'antispam_delay_error_msg_placeholder' => 'Príliš rýchle odoslanie formulára! prosím skúste to za pár sékúnd znovu!',
+ 'add_google_recaptcha' => 'Pridať Google reCaptcha',
+ 'add_google_recaptcha_comment' => 'Pridá reCaptcha do kontaktného formulára (viac informácií v súbore README.md). API kľúče môžete získať na stránke Google reCaptcha .',
+ 'google_recaptcha_site_key' => 'Site key',
+ 'google_recaptcha_site_key_comment' => 'Vložte svoj "site key"',
+ 'google_recaptcha_secret_key' => 'Secret key',
+ 'google_recaptcha_secret_key_comment' => 'Vložte svoj "secret key"',
+ 'google_recaptcha_error_msg' => 'Chybová správa',
+ 'google_recaptcha_error_msg_comment' => 'Správa, ktorá sa zobrazí, pokiaľ dôjde k chybe pri overení reCAPTCHA.',
+ 'google_recaptcha_error_msg_placeholder' => 'Chyba pri overení pomocou Google reCAPTCHA!',
+ 'google_recaptcha_scripts_allow' => 'Automaticky pridať Google reCAPTCHA skript',
+ 'google_recaptcha_scripts_allow_comment' => 'Vloží odkaz na JavaScriptový súbor potrebný pre fungovanie reCAPTCHA.',
+ 'google_recaptcha_locale_allow' => 'Povoliť detekciu jazyka',
+ 'google_recaptcha_locale_allow_comment' => 'Pridá k reCAPTCHA skriptu kód jazyka stránky, takže overovací box bude preložený do jazyka návštevníka webu.',
+ 'add_ip_pretection' => 'Testovať IP adresu odosielateľa',
+ 'add_ip_pretection_comment' => 'Nepovolí príliš mnoho odoslaní formulára z jednej IP adresy',
+ 'add_ip_pretection_count' => 'Maximálny počet odoslaní behom jedného dňa',
+ 'add_ip_pretection_count_comment' => 'Počet povolených odoslaní formulára z jednej IP adresy behom jedného dňa',
+ 'add_ip_pretection_count_placeholder' => '3',
+ 'add_ip_pretection_error_get_ip' => 'Nepodarilo sa určiť vašu IP adresu!',
+ 'add_ip_pretection_error_too_many_submits' => 'Chybová správa pri prekročení počtu odoslaní',
+ 'add_ip_pretection_error_too_many_submits_comment' => 'Správa, ktorú obdrží uživateľ pri prekročení limitu počtu odoslaní formulára',
+ 'add_ip_pretection_error_too_many_submits_placeholder' => 'Bol prekročný limit odoslaní formulára behom jedného dňa!',
+ 'disabled_extensions' => 'Zakázané rozšírenia',
+ 'disabled_extensions_comment' => 'Nastavenia zo záložky Súkromie spôsibili vypnutie týchto rozšírení',
+ ],
+ 'mapping' => [
+ 'hint' => [
+ 'title' => 'zrušiť vazby na stĺpce?',
+ 'content' => '
+ Môžete vytvoriť ľubovolný formulár s vlastnými poliami a ich typmi.
+ Systém zapíše do databázy všetky odoslané dáta formulára, ale pre Prehľad správ sú zvlášť ukladané polia Meno, Email a Správa.
+ preto je nutné identifikovať pre tieot stĺpce odpovedajúce polia vo vašom formulári.
+ Vytvorené väzby sú použité aj pri odosielaní automatických odpovedí, kde je nutná väzba aspoň na pole Email.
+ ',
+ ],
+ 'warning' => [
+ 'title' => 'Nevidíte vaše formulárové polia?',
+ 'content' => '
+ Pokiaľ tu nevidíte svoje formulárové polia, kliknite dole na tlačidlo Uložiť a potom obnovte stránku (F5 alebo Ctrl+R / Cmd+R).
+ ',
+ ],
+ ],
+
+ 'privacy' => [
+ 'disable_messages_saving' => 'Zakázať ukladanie správ',
+ 'disable_messages_saving_comment' => 'Pokiaľ je zaškrtnuté, odoslané správy sa nebudú ukladať do databázy.Táto voľba zárovaň zakáže použitie IP ochrany! ',
+ 'disable_messages_saving_comment_section' => '',
+ ],
+ 'tabs' => [
+ 'form' => 'Formulár',
+ 'buttons' => 'Odosielacie tlačidlo',
+ 'form_fields' => 'Pole formulára',
+ 'mapping' => 'Väzby stĺpcov',
+ 'email' => 'Email',
+ 'antispam' => 'Antispam',
+ 'privacy' => 'Súkromie'
+ ],
+ ],
+ 'components' => [
+ 'groups' => [
+ 'hacks' => 'Hacks',
+ ],
+ 'preperties' => [
+ 'disable_notifications' => 'Zakázať odosielanie notifikačných emailov',
+ 'disable_notifications_comment' => 'Zakáže odoslanie notifikačných emailov (bez ohľadu na systémové nastavenia formulára)',
+ 'form_description' => 'Popisok formulára',
+ 'form_description_comment' => 'Volitelne môžete pridať popisok formulára, ktorý sa uloží spoločne s odoslanými dátami zoznamu správ. Môžete použiť aj {{ :slug }}.',
+ ]
+ ],
+];
diff --git a/plugins/janvince/smallcontactform/models/Message.php b/plugins/janvince/smallcontactform/models/Message.php
new file mode 100644
index 000000000..46782ec94
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/Message.php
@@ -0,0 +1,540 @@
+ 'System\Models\File',
+ ];
+
+ /**
+ * Scope new messages only
+ */
+ public function scopeIsNew($query)
+ {
+ return $query->where('new_message', '=', 1);
+ }
+
+ /**
+ * Scope read messages only
+ */
+ public function scopeIsRead($query)
+ {
+ return $query->where('new_message', '=', 0);
+ }
+
+
+ public function storeFormData($data, $formAlias, $formDescription){
+
+ if(Settings::getTranslated('privacy_disable_messages_saving')) {
+ return;
+ }
+
+ $output = [];
+ $name_field_value = NULL;
+ $email_field_value = NULL;
+ $message_field_value = NULL;
+
+ $formFields = Settings::getTranslated('form_fields');
+
+ $uploads = [];
+
+ foreach($data as $key => $value) {
+
+ // skip helpers
+ if(substr($key, 0, 1) == '_'){
+ continue;
+ }
+
+ // skip reCaptcha
+ if($key == 'g-recaptcha-response'){
+ continue;
+ }
+
+ // skip non-defined fields
+ // store uploaded file -- currently only one file is supported
+ $fieldDefined = null;
+ $fieldUpload = null;
+ foreach( $formFields as $field) {
+ if( $field['name'] == $key) {
+ $fieldDefined = true;
+
+ if($field['type'] == 'file') {
+ $fieldUpload = true;
+
+ if(!empty(Input::file($key))) {
+ $file = new File;
+ $file->data = (Input::file($key));
+ $file->is_public = true;
+ $file->save();
+ $uploads[] = $file;
+ }
+ }
+
+ }
+ }
+
+ // skip uploads
+ if($fieldUpload){
+ continue;
+ }
+
+ if( !$fieldDefined ) {
+ Log::warning('SMALL CONTACT FORM WARNING: Found a non-defined field in sent data! Field name: ' . e($key) . ' and value: ' . e($value['value']) );
+ continue;
+ }
+
+ $output[$key] = e($value['value']);
+
+ // if email field is assigned in autoreply, save it separatelly
+ if(empty($email_field_value) and $key == Settings::getTranslated('autoreply_email_field')){
+ $email_field_value = e($value['value']);
+ }
+
+ // if name field is assigned in autoreply, save it separatelly
+ if(empty($name_field_value) and $key == Settings::getTranslated('autoreply_name_field')){
+ $name_field_value = e($value['value']);
+ }
+
+ // if message is assigned in autoreply, save it separatelly
+ if(empty($message_field_value) and $key == Settings::getTranslated('autoreply_message_field')){
+ $message_field_value = e($value['value']);
+ }
+
+ }
+
+ $this->form_data = $output;
+ $this->name = $name_field_value;
+ $this->email = $email_field_value;
+ $this->message = $message_field_value;
+ $this->remote_ip = Request::ip();
+ $this->form_alias = $formAlias;
+ $this->form_description = $formDescription;
+ $this->url = url()->full();
+ $this->save();
+
+ // Add files
+ if($uploads) {
+
+ foreach($uploads as $upload) {
+ $this->uploads()->add($upload);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Build and send autoreply message
+ */
+ public function sendAutoreplyEmail($postData, $componentProperties = [], $formAlias, $formDescription, $messageObject){
+
+ if(!Settings::getTranslated('allow_autoreply')) {
+ return;
+ }
+
+ if (!empty($componentProperties['disable_autoreply'])) {
+ return;
+ }
+
+ if(!Settings::getTranslated('autoreply_email_field')) {
+ Log::error('SMALL CONTACT FORM ERROR: Contact form data have no email to send autoreply message!');
+ return;
+ }
+
+ /**
+ * Extract and test email field value
+ */
+ $sendTo = '';
+
+ foreach($postData as $key => $field) {
+ if($key == Settings::getTranslated('autoreply_email_field')){
+ $sendTo = $field['value'];
+ }
+ }
+
+ $validator = Validator::make(['email' => $sendTo], ['email' => 'required|email']);
+
+ if($validator->fails()){
+ Log::error('SMALL CONTACT FORM ERROR: Email to send autoreply is not valid!' . PHP_EOL . ' Data: '. json_encode($postData) );
+ return;
+ }
+
+ if( Settings::getTranslated('allow_email_queue') ){
+ $method = 'queue';
+ } else {
+ $method = 'send';
+ }
+
+ $output = [];
+ $outputFull = [];
+ $formFields = Settings::getTranslated('form_fields');
+
+ foreach($formFields as $field) {
+
+ if(!empty($field['type'] and $field['type'] == 'file')) {
+ continue;
+ }
+
+ $fieldValue = null;
+
+ if( !empty( $postData[ $field['name'] ]['value'] ) ) {
+ $fieldValue = e( html_entity_decode( $postData[ $field['name'] ]['value'] ) );
+ } else {
+ $fieldValue = null;
+ }
+
+ if( !empty( $field['name'] ) ) {
+ $outputFull[ $field['name'] ] = array_merge( $field, [ 'value' => $fieldValue ] );
+ }
+
+ $output[ $field['name'] ] = $fieldValue;
+
+ }
+
+ $output['form_description'] = $formDescription;
+ $output['form_alias'] = $formAlias;
+
+ $template = Settings::getTranslatedTemplates('en', App::getLocale(), 'autoreply');
+
+ if( Settings::getTranslated('email_template') ){
+
+ if(View::exists(Settings::getTranslated('email_template')) OR !empty( MailTemplate::listAllTemplates()[Settings::getTranslated('email_template')] ) ) {
+ $template = Settings::getTranslated('email_template');
+ } else {
+ Log::error('SMALL CONTACT FORM: Missing defined email template: ' . Settings::getTranslated('email_template') . '. Default template will be used!');
+ }
+
+ }
+
+ /**
+ * Override email template by component property
+ * Language specific template has priority (override non language specific)
+ */
+ if ( !empty($componentProperties['autoreply_template']) and !empty( MailTemplate::listAllTemplates()[ $componentProperties['autoreply_template'] ] ) ) {
+ $template = $componentProperties['autoreply_template'];
+ } elseif ( !empty($componentProperties['autoreply_template']) and empty( MailTemplate::listAllTemplates()[ $componentProperties['autoreply_template'] ] ) ) {
+ Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties['autoreply_template'] . '. ' . $template . ' template will be used!');
+ }
+
+ if ( !empty($componentProperties[ ('autoreply_template_'.App::getLocale())]) and !empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('autoreply_template_'.App::getLocale())] ] ) ) {
+ $template = $componentProperties[('autoreply_template_'.App::getLocale())];
+ } elseif ( !empty($componentProperties[ ('autoreply_template_'.App::getLocale())]) and empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('autoreply_template_'.App::getLocale())] ] ) ) {
+ Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties[ ('autoreply_template_'.App::getLocale())] . '. ' . $template . ' template will be used!');
+ }
+
+ if(!empty($messageObject->uploads))
+ {
+ $uploads = $messageObject->uploads;
+ }
+ else
+ {
+ $uploads = [];
+ }
+
+ Mail::{$method}($template, ['messageObject' => $messageObject, 'uploads' => $uploads, 'fields' => $output, 'fieldsDetails' => $outputFull, 'url' => url()->full()], function($message) use($sendTo, $componentProperties, $output){
+ $message->to($sendTo);
+
+ if (!empty($componentProperties['autoreply_subject'])) {
+ $message->subject(Twig::parse($componentProperties['autoreply_subject'],['fields' => $output]));
+
+ \App::forgetInstance('parse.twig');
+ \App::forgetInstance('twig.environment');
+
+ } elseif (Settings::getTranslated('email_subject')) {
+ $message->subject(Twig::parse(Settings::getTranslated('email_subject'), ['fields' => $output]));
+
+ \App::forgetInstance('parse.twig');
+ \App::forgetInstance('twig.environment');
+
+ }
+
+ /**
+ * From address
+ * Component's property can override this
+ */
+ $fromAddress = null;
+ $fromAddressName = null;
+
+ if( Settings::getTranslated('email_address_from') ) {
+ $fromAddress = Settings::getTranslated('email_address_from');
+ $fromAddressName = Settings::getTranslated('email_address_from_name');
+ }
+
+ if( !empty($componentProperties['autoreply_address_from']) ) {
+ $fromAddress = $componentProperties['autoreply_address_from'];
+ }
+
+ if( !empty($componentProperties['autoreply_address_from_name']) ) {
+ $fromAddressName = $componentProperties['autoreply_address_from_name'];
+ }
+
+ if( !empty($componentProperties[ ('autoreply_address_from_name_'.App::getLocale()) ]) ) {
+ $fromAddressName = $componentProperties[ ('autoreply_address_from_name_'.App::getLocale()) ];
+ }
+
+ $validator = Validator::make(['email' => $fromAddress], ['email' => 'required|email']);
+
+ if($validator->fails()){
+ Log::error('SMALL CONTACT FORM ERROR: Autoreply email address is invalid (' .$fromAddress. ')! System email address and name will be used.');
+ } else {
+ $message->from($fromAddress, $fromAddressName);
+ }
+
+
+ /**
+ * Reply To address
+ * Component's property can override this
+ */
+ $replyToAddress = null;
+
+ if( Settings::getTranslated('email_address_replyto') ) {
+ $replyToAddress = Settings::getTranslated('email_address_replyto');
+ }
+
+ if( !empty($componentProperties['autoreply_address_replyto']) ) {
+ $replyToAddress = $componentProperties['autoreply_address_replyto'];
+ }
+
+ if( $replyToAddress ) {
+
+ $validator = Validator::make(['email' => $replyToAddress], ['email' => 'required|email']);
+
+ if($validator->fails()){
+ Log::error('SMALL CONTACT FORM ERROR: Autoreply Reply To email address is invalid (' .$replyToAddress. ')! No Reply To header will be added.');
+ } else {
+ $message->replyTo($replyToAddress);
+ }
+
+ }
+ });
+
+ }
+
+ /**
+ * Build and send notification message
+ */
+ public function sendNotificationEmail($postData, $componentProperties = [], $formAlias, $formDescription, $messageObject){
+
+ if(!Settings::getTranslated('allow_notifications')) {
+ return;
+ }
+
+ if(!empty($componentProperties['disable_notifications'])) {
+ return;
+ }
+
+ $sendTo = (!empty($componentProperties['notification_address_to']) ? $componentProperties['notification_address_to'] : Settings::getTranslated('notification_address_to') );
+
+ $sendToAddresses = explode(',', $sendTo);
+ $sendToAddressesValidated = [];
+
+ foreach($sendToAddresses as $sendToAddress) {
+
+ $validator = Validator::make(['email' => trim($sendToAddress)], ['email' => 'required|email']);
+
+ if($validator->fails()){
+ Log::error('SMALL CONTACT FORM ERROR: Notification email address (' .trim($sendToAddress). ') is invalid! No notification will be delivered!');
+ } else {
+ $sendToAddressesValidated[] = trim($sendToAddress);
+ }
+ }
+
+ if( !count($sendToAddressesValidated) ) {
+ return;
+ }
+
+ if( Settings::getTranslated('allow_email_queue') ){
+ $method = 'queue';
+ } else {
+ $method = 'send';
+ }
+
+ $output = [];
+ $outputFull = [];
+ $formFields = Settings::getTranslated('form_fields');
+ $replyToAddress = null;
+ $replyToName = null;
+
+ foreach($formFields as $field) {
+
+ if(!empty($field['type'] and $field['type'] == 'file')) {
+ continue;
+ }
+
+ $fieldValue = null;
+
+ if( !empty( $postData[ $field['name'] ]['value'] ) ) {
+ $fieldValue = e( html_entity_decode( $postData[ $field['name'] ]['value'] ) );
+ } else {
+ $fieldValue = null;
+ }
+
+ if( !empty( $field['name'] ) ) {
+ $outputFull[ $field['name'] ] = array_merge( $field, [ 'value' => $fieldValue ] );
+ }
+
+ // If email field is assigned, prepare for replyTo
+ if(empty($replyToAddress) and $field['name'] == Settings::getTranslated('autoreply_email_field') and isset($postData[$field['name']]['value'])){
+ $replyToAddress = e( $postData[$field['name']]['value'] );
+ }
+
+ // If name field is assigned, prepare for fromAddress
+ if(empty($replyToName) and $field['name'] == Settings::getTranslated('autoreply_name_field') and isset($postData[$field['name']]['value'])){
+ $replyToName = e( $postData[$field['name']]['value'] );
+ }
+
+ $output[ $field['name'] ] = $fieldValue;
+
+ }
+
+ $output['form_description'] = $formDescription;
+ $output['form_alias'] = $formAlias;
+
+ $template = Settings::getTranslatedTemplates('en', App::getLocale(), 'notification');
+
+ if( Settings::getTranslated('notification_template') ){
+
+ if(View::exists(Settings::getTranslated('notification_template')) OR !empty( MailTemplate::listAllTemplates()[Settings::getTranslated('notification_template')] ) ) {
+ $template = Settings::getTranslated('notification_template');
+ } else {
+ Log::error('SMALL CONTACT FORM: Missing defined email template: ' . Settings::getTranslated('notification_template') . '. Default template will be used!');
+ }
+
+ }
+
+ /**
+ * Override email template by component property
+ * Language specific template has priority (override non language specific)
+ */
+ if ( !empty($componentProperties['notification_template']) and !empty( MailTemplate::listAllTemplates()[ $componentProperties['notification_template'] ] ) ) {
+ $template = $componentProperties['notification_template'];
+ } elseif ( !empty($componentProperties['notification_template']) and empty( MailTemplate::listAllTemplates()[ $componentProperties['notification_template'] ] ) ) {
+ Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties['notification_template'] . '. ' . $template . ' template will be used!');
+ }
+
+
+ if ( !empty($componentProperties[ ('notification_template_'.App::getLocale())]) and !empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('notification_template_'.App::getLocale())] ] ) ) {
+ $template = $componentProperties[('notification_template_'.App::getLocale())];
+ } elseif ( !empty($componentProperties[ ('notification_template_'.App::getLocale())]) and empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('notification_template_'.App::getLocale())] ] ) ) {
+ Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties[ ('notification_template_'.App::getLocale())] . '. ' . $template . ' template will be used!');
+ }
+
+ if(!empty($messageObject->uploads))
+ {
+ $uploads = $messageObject->uploads;
+ }
+ else
+ {
+ $uploads = [];
+ }
+
+ Mail::{$method}($template, ['messageObject' => $messageObject, 'uploads' => $uploads, 'fields' => $output, 'fieldsDetails' => $outputFull, 'url' => url()->full()], function($message) use($sendToAddressesValidated, $replyToAddress, $replyToName, $componentProperties, $output){
+
+ if( count($sendToAddressesValidated)>1 ) {
+
+ foreach($sendToAddressesValidated as $address) {
+ $message->bcc($address);
+ }
+ } elseif( !empty($sendToAddressesValidated[0]) ) {
+ $message->to($sendToAddressesValidated[0]);
+ } else {
+ return;
+ }
+
+ if (!empty($componentProperties['notification_subject'])) {
+ $message->subject(Twig::parse($componentProperties['notification_subject'], ['fields' => $output]));
+
+ \App::forgetInstance('parse.twig');
+ \App::forgetInstance('twig.environment');
+ }
+
+ /**
+ * Set Reply to address and also set From address if requested
+ */
+ if ( !empty($replyToAddress) ) {
+
+ $validator = Validator::make(['email' => $replyToAddress], ['email' => 'required|email']);
+
+ if($validator->fails()){
+ Log::error('SMALL CONTACT FORM ERROR: Notification replyTo address is not valid (' .$replyToAddress. ')! Reply to address will not be used.');
+ return;
+ }
+
+ $message->replyTo($replyToAddress, $replyToName);
+
+ // Force From field if requested
+ if ( Settings::getTranslated('notification_address_from_form') ) {
+ $message->from($replyToAddress, $replyToName);
+ }
+ }
+
+ /**
+ * Set From address if defined in component property
+ */
+ if ( !empty($componentProperties['notification_address_from'])) {
+
+ if (!empty($componentProperties['notification_address_from_name'])) {
+ $fromAddressName = $componentProperties['notification_address_from_name'];
+ } else {
+ $fromAddressName = null;
+ }
+
+ $message->from($componentProperties['notification_address_from'], $fromAddressName);
+ }
+
+ });
+ }
+
+ /**
+ * Test how many times was given IP address used this day
+ * @return int
+ */
+ public function testIPAddress($ip){
+
+ $today = Carbon::today();
+
+ $count = Message::whereDate('created_at', '=', $today->toDateString())
+ ->where('remote_ip', '=', $ip)
+ ->count();
+
+ return $count;
+
+ }
+
+}
diff --git a/plugins/janvince/smallcontactform/models/MessageExport.php b/plugins/janvince/smallcontactform/models/MessageExport.php
new file mode 100644
index 000000000..ab0868eaa
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/MessageExport.php
@@ -0,0 +1,29 @@
+each(function($record) use ($columns) {
+
+ $record->addVisible($columns);
+ });
+
+ return $records->toArray();
+ }
+}
diff --git a/plugins/janvince/smallcontactform/models/Settings.php b/plugins/janvince/smallcontactform/models/Settings.php
new file mode 100644
index 000000000..c62be804f
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/Settings.php
@@ -0,0 +1,438 @@
+findByIdentifier('Rainlab.Translate');
+
+ if ($pluginManager && !$pluginManager->disabled) {
+
+ $settings = Settings::instance();
+
+ $valueTranslated = $settings->getAttributeTranslated($value);
+
+ if(!empty($valueTranslated)){
+ return $valueTranslated;
+ } else {
+ return $defaultValue;
+ }
+
+ } else {
+ return Settings::get($value, $defaultValue);
+ }
+
+ }
+
+ /**
+ * Try to use Rainlab Tranlaste plugin to get translated content for given key
+ */
+ public static function getDictionaryTranslated($value){
+
+ // Check for Rainlab.Translate plugin
+ $translatePlugin = PluginManager::instance()->findByIdentifier('Rainlab.Translate');
+
+ if ($translatePlugin && !$translatePlugin->disabled) {
+
+ $params = [];
+
+ $message = \RainLab\Translate\Models\Message::trans($value, $params);
+
+ return $message;
+
+ } else {
+ return $value;
+ }
+
+ }
+
+
+
+ /**
+ * Generate form fields types list
+ * @return array
+ */
+ public function getTypeOptions($value, $formData)
+ {
+
+ $fieldTypes = $this->getFieldTypes();
+
+ $types = [];
+
+ if(!$fieldTypes) {
+ return [];
+ }
+
+ foreach ($fieldTypes as $key => $value) {
+ $types[$key] = 'janvince.smallcontactform::lang.settings.form_field_types.'.$key;
+ }
+
+ return $types;
+
+ }
+
+ /**
+ * Generate form fields types list
+ * @return array
+ */
+ public function getValidationTypeOptions($value, $formData)
+ {
+
+ return [
+ 'required' => 'janvince.smallcontactform::lang.settings.form_field_validation.required',
+ 'email' => 'janvince.smallcontactform::lang.settings.form_field_validation.email',
+ 'numeric' => 'janvince.smallcontactform::lang.settings.form_field_validation.numeric',
+ 'custom' => 'janvince.smallcontactform::lang.settings.form_field_validation.custom',
+ ];
+ }
+
+ /**
+ * Generate list of existing fields for email name
+ * @return array
+ */
+ public function getAutoreplyNameFieldOptions($value, $formData)
+ {
+
+ return $this->getFieldsList('text');
+
+ }
+
+ /**
+ * Generate list of existing fields for email name
+ * @return array
+ */
+ public function getAutoreplyEmailFieldOptions($value, $formData)
+ {
+
+ return $this->getFieldsList('email');
+
+ }
+
+ /**
+ * Generate list of existing message fields
+ * @return array
+ */
+ public function getAutoreplyMessageFieldOptions($value, $formData)
+ {
+
+ return $this->getFieldsList('textarea');
+
+ }
+
+ /**
+ * Generate fields list
+ * @return arry
+ */
+ private function getFieldsList($type = false, $forceType = false){
+
+ $output = [];
+ $outputAll = [];
+
+ foreach (Settings::getTranslated('form_fields', []) as $field) {
+
+ $fieldName = $field['name'] . ' ['. $field ['type'] . ']';
+
+ $outputAll[$field['name']] = $fieldName;
+
+ if($type && !empty($field['type']) && $field['type'] <> $type) {
+ continue;
+ } else {
+ $output[$field['name']] = $fieldName;
+ }
+
+ }
+
+ if($forceType) {
+ return $output;
+ } else {
+ return $outputAll;
+ }
+
+ }
+
+ /**
+ * HTML field types mapping array
+ * @return array
+ */
+ public static function getFieldTypes($type = false) {
+
+ $types = [
+
+ 'text' => [
+ 'html_open' => 'input',
+ 'label' => true,
+ 'wrapper_class' => 'form-group',
+ 'field_class' => 'form-control',
+ 'use_name_attribute' => true,
+ 'attributes' => [
+ 'type' => 'text',
+ ],
+ 'html_close' => null,
+ ],
+
+ 'email' => [
+ 'html_open' => 'input',
+ 'label' => true,
+ 'wrapper_class' => 'form-group',
+ 'field_class' => 'form-control',
+ 'use_name_attribute' => true,
+ 'attributes' => [
+ 'type' => 'email',
+ ],
+ 'html_close' => null,
+ ],
+
+ 'textarea' => [
+ 'html_open' => 'textarea',
+ 'label' => true,
+ 'wrapper_class' => 'form-group',
+ 'field_class' => 'form-control',
+ 'use_name_attribute' => true,
+ 'attributes' => [
+ 'rows' => 5,
+ ],
+ 'html_close' => 'textarea',
+ ],
+
+ 'checkbox' => [
+ 'html_open' => 'input',
+ 'label' => false,
+ 'wrapper_class' => null,
+ 'field_class' => null,
+ 'inner_label' => true,
+ 'use_name_attribute' => true,
+ 'attributes' => [
+ 'type' => 'checkbox',
+ ],
+ 'html_close' => null,
+ ],
+
+ 'dropdown' => [
+ 'html_open' => 'select',
+ 'label' => true,
+ 'wrapper_class' => 'form-group',
+ 'field_class' => 'form-control',
+ 'inner_label' => false,
+ 'use_name_attribute' => true,
+ 'attributes' => [
+ ],
+ 'html_close' => 'select',
+ ],
+
+ 'file' => [
+ 'html_open' => 'input',
+ 'label' => true,
+ 'wrapper_class' => 'form-group',
+ 'field_class' => 'form-control',
+ 'inner_label' => false,
+ 'use_name_attribute' => true,
+ 'attributes' => [
+ 'type' => 'file',
+ ],
+ 'html_close' => null,
+ ],
+
+ 'custom_code' => [
+ 'html_open' => "div",
+ 'label' => true,
+ 'wrapper_class' => null,
+ 'field_class' => null,
+ 'inner_label' => false,
+ 'use_name_attribute' => false,
+ 'attributes' => [
+ ],
+ 'html_close' => "div",
+ ],
+
+ 'custom_content' => [
+ 'html_open' => "div",
+ 'label' => true,
+ 'wrapper_class' => null,
+ 'field_class' => null,
+ 'inner_label' => false,
+ 'use_name_attribute' => false,
+ 'attributes' => [
+ ],
+ 'html_close' => "div",
+ ],
+
+ ];
+
+ if($type){
+ if(!empty($types[$type])){
+ return $types[$type];
+ }
+ }
+
+ return $types;
+
+ }
+
+ /**
+ * Get non English locales from Translate plugin
+ * @return array
+ */
+ public static function getEnabledLocales() {
+
+ // Check for Rainlab.Translate plugin
+ $pluginManager = PluginManager::instance()->findByIdentifier('Rainlab.Translate');
+
+ if ($pluginManager && !$pluginManager->disabled) {
+
+ $locales = \RainLab\Translate\Models\Locale::listEnabled();
+
+ return $locales;
+
+ } elseif( App::getLocale() ) {
+ // Backend locale
+ return [
+ 'en' => 'English',
+ App::getLocale() => App::getLocale(),
+ ];
+ }
+
+ // English fallback
+ return [
+ 'en' => 'English',
+ ];
+
+ }
+
+ /**
+ * Get non English locales from Translate plugin
+ * @return array
+ */
+ public static function getTranslatedTemplates($defaultLocale = 'en', $locale = NULL, $templateType = NULL) {
+
+ $enabledLocales = Settings::getEnabledLocales();
+
+ /**
+ * Templates map
+ * [locale] => [templateType] => [template]
+ */
+ $translatedTemplates = [
+
+ 'en' => [
+
+ 'autoreply' => [
+ 'janvince.smallcontactform::mail.autoreply' => 'janvince.smallcontactform::lang.mail.templates.autoreply',
+ ],
+
+ 'notification' => [
+ 'janvince.smallcontactform::mail.notification' => 'janvince.smallcontactform::lang.mail.templates.notification',
+ ],
+
+ ],
+
+ 'cs' => [
+
+ 'autoreply' => [
+ 'janvince.smallcontactform::mail.autoreply_cs' => 'janvince.smallcontactform::lang.mail.templates.autoreply_cs',
+ ],
+
+ 'notification' => [
+ 'janvince.smallcontactform::mail.notification_cs' => 'janvince.smallcontactform::lang.mail.templates.notification_cs',
+ ],
+
+ ],
+
+ 'es' => [
+
+ 'autoreply' => [
+ 'janvince.smallcontactform::mail.autoreply_es' => 'janvince.smallcontactform::lang.mail.templates.autoreply_es',
+ ],
+
+ 'notification' => [
+ 'janvince.smallcontactform::mail.notification_es' => 'janvince.smallcontactform::lang.mail.templates.notification_es',
+ ],
+
+ ],
+
+ ];
+
+ if( $locale and $templateType ) {
+
+ if( !empty($translatedTemplates[$locale]) and !empty($translatedTemplates[$locale][$templateType]) ) {
+ return key($translatedTemplates[$locale][$templateType]);
+ } elseif ( $defaultLocale and !empty($translatedTemplates[$defaultLocale]) and !empty($translatedTemplates[$defaultLocale][$templateType]) ) {
+ return key($translatedTemplates[$defaultLocale][$templateType]);
+ } else {
+ return NULL;
+ }
+
+
+
+ } else {
+
+ $allEnabledTemplates = [];
+
+ foreach( $enabledLocales as $enabledLocaleKey => $enabledLocaleName ) {
+
+ if( !empty($translatedTemplates[$enabledLocaleKey]) ) {
+
+ foreach( $translatedTemplates[$enabledLocaleKey] as $type ) {
+
+ foreach( $type as $key => $value ) {
+ $allEnabledTemplates[$key] = e(trans($value));
+
+ }
+
+ }
+
+ }
+
+ }
+
+ return $allEnabledTemplates;
+
+ }
+
+ return [];
+
+ }
+
+}
diff --git a/plugins/janvince/smallcontactform/models/message/columns.yaml b/plugins/janvince/smallcontactform/models/message/columns.yaml
new file mode 100644
index 000000000..5c980d30d
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/message/columns.yaml
@@ -0,0 +1,67 @@
+# ===================================
+# Column Definitions
+# ===================================
+
+columns:
+
+ # id:
+ # label: janvince.smallgallery::lang.categories.columns.id
+ # searchable: true
+
+ new_message:
+ label: janvince.smallcontactform::lang.models.message.columns.new_message
+ type: switch_icon_star
+
+ created_at:
+ label: janvince.smallcontactform::lang.models.message.columns.datetime
+ type: datetime
+ searchable: true
+
+ name:
+ label: janvince.smallcontactform::lang.models.message.columns.name
+ searchable: true
+
+ email:
+ label: janvince.smallcontactform::lang.models.message.columns.email
+ searchable: true
+
+ message:
+ label: janvince.smallcontactform::lang.models.message.columns.message
+ searchable: true
+ type: text_preview
+ width: 50%
+
+ remote_ip:
+ label: janvince.smallcontactform::lang.models.message.columns.remote_ip
+ searchable: true
+ type: text
+ invisible: true
+
+ form_alias:
+ label: janvince.smallcontactform::lang.models.message.columns.form_alias
+ searchable: true
+ type: text
+ invisible: true
+
+ form_description:
+ label: janvince.smallcontactform::lang.models.message.columns.form_description
+ searchable: true
+ type: text_preview
+ invisible: true
+
+ form_data:
+ label: janvince.smallcontactform::lang.models.message.columns.form_data
+ searchable: true
+ type: text
+ invisible: true
+
+ url:
+ label: janvince.smallcontactform::lang.models.message.columns.url
+ searchable: true
+ type: text
+ invisible: true
+
+ uploads:
+ label: janvince.smallcontactform::lang.models.message.columns.files
+ type: scf_files_link
+ invisible: true
diff --git a/plugins/janvince/smallcontactform/models/message/columns_export.yaml b/plugins/janvince/smallcontactform/models/message/columns_export.yaml
new file mode 100644
index 000000000..6231f7d6e
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/message/columns_export.yaml
@@ -0,0 +1,49 @@
+# ===================================
+# Column Definitions
+# ===================================
+
+columns:
+
+ id:
+ label: janvince.smallcontactform::lang.models.message.columns.id
+ type: number
+
+ new_message:
+ label: janvince.smallcontactform::lang.models.message.columns.new
+ type: number
+
+ created_at:
+ label: janvince.smallcontactform::lang.models.message.columns.created_at
+ type: datetime
+
+ updated_at:
+ label: janvince.smallcontactform::lang.models.message.columns.updated_at
+ type: datetime
+
+ name:
+ label: janvince.smallcontactform::lang.models.message.columns.name
+ type: text
+
+ email:
+ label: janvince.smallcontactform::lang.models.message.columns.email
+ type: text
+
+ message:
+ label: janvince.smallcontactform::lang.models.message.columns.message
+ type: text
+
+ remote_ip:
+ label: janvince.smallcontactform::lang.models.message.columns.remote_ip
+ type: text
+
+ form_alias:
+ label: janvince.smallcontactform::lang.models.message.columns.form_alias
+ type: text
+
+ form_description:
+ label: janvince.smallcontactform::lang.models.message.columns.form_description
+ type: text
+
+ form_description:
+ label: janvince.smallcontactform::lang.models.message.columns.url
+ type: text
diff --git a/plugins/janvince/smallcontactform/models/settings/_mapping_help.htm b/plugins/janvince/smallcontactform/models/settings/_mapping_help.htm
new file mode 100644
index 000000000..de170b0de
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/settings/_mapping_help.htm
@@ -0,0 +1,9 @@
+
diff --git a/plugins/janvince/smallcontactform/models/settings/_mapping_warning.htm b/plugins/janvince/smallcontactform/models/settings/_mapping_warning.htm
new file mode 100644
index 000000000..5c8b8ff5c
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/settings/_mapping_warning.htm
@@ -0,0 +1,9 @@
+
diff --git a/plugins/janvince/smallcontactform/models/settings/fields.yaml b/plugins/janvince/smallcontactform/models/settings/fields.yaml
new file mode 100644
index 000000000..200d98ab9
--- /dev/null
+++ b/plugins/janvince/smallcontactform/models/settings/fields.yaml
@@ -0,0 +1,870 @@
+fields: { }
+tabs:
+ fields:
+
+ ## FORM
+
+ form_css_class:
+ label: 'janvince.smallcontactform::lang.settings.form.css_class'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_success_msg:
+ label: 'janvince.smallcontactform::lang.settings.form.success_msg'
+ placeholder: 'janvince.smallcontactform::lang.settings.form.success_msg_placeholder'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_error_msg:
+ label: 'janvince.smallcontactform::lang.settings.form.error_msg'
+ placeholder: 'janvince.smallcontactform::lang.settings.form.error_msg_placeholder'
+ span: right
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_hide_after_success:
+ label: 'janvince.smallcontactform::lang.settings.form.hide_after_success'
+ comment: 'janvince.smallcontactform::lang.settings.form.hide_after_success_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_use_placeholders:
+ label: 'janvince.smallcontactform::lang.settings.form.use_placeholders'
+ comment: 'janvince.smallcontactform::lang.settings.form.use_placeholders_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_disable_browser_validation:
+ label: 'janvince.smallcontactform::lang.settings.form.disable_browser_validation'
+ comment: 'janvince.smallcontactform::lang.settings.form.disable_browser_validation_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+
+ section_ajax:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_allow_ajax:
+ label: 'janvince.smallcontactform::lang.settings.form.allow_ajax'
+ comment: 'janvince.smallcontactform::lang.settings.form.allow_ajax_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ form_allow_confirm_msg:
+ label: 'janvince.smallcontactform::lang.settings.form.allow_confirm_msg'
+ comment: 'janvince.smallcontactform::lang.settings.form.allow_confirm_msg_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: form_allow_ajax
+ condition: checked
+
+ form_send_confirm_msg:
+ label: 'janvince.smallcontactform::lang.settings.form.send_confirm_msg'
+ placeholder: 'janvince.smallcontactform::lang.settings.form.send_confirm_msg_placeholder'
+ span: right
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: form_allow_ajax
+ condition: checked
+
+ section_assets:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+ trigger:
+ action: show
+ field: add_assets
+ condition: checked
+
+ add_assets:
+ label: 'janvince.smallcontactform::lang.settings.form.add_assets'
+ comment: 'janvince.smallcontactform::lang.settings.form.add_assets_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+
+ add_css_assets:
+ label: 'janvince.smallcontactform::lang.settings.form.add_css_assets'
+ comment: 'janvince.smallcontactform::lang.settings.form.add_css_assets_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_assets
+ condition: checked
+
+ add_js_assets:
+ label: 'janvince.smallcontactform::lang.settings.form.add_js_assets'
+ comment: 'janvince.smallcontactform::lang.settings.form.add_js_assets_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_assets
+ condition: checked
+
+ ## BUTTONS
+
+ send_btn_wrapper_css:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.buttons.send_btn_wrapper_css'
+ placeholder: 'janvince.smallcontactform::lang.settings.buttons.send_btn_wrapper_css_placeholder'
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+
+ send_btn_css_class:
+ label: 'janvince.smallcontactform::lang.settings.buttons.send_btn_css_class'
+ placeholder: 'janvince.smallcontactform::lang.settings.buttons.send_btn_css_class_placeholder'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+
+ send_btn_text:
+ label: 'janvince.smallcontactform::lang.settings.buttons.send_btn_text'
+ placeholder: 'janvince.smallcontactform::lang.settings.buttons.send_btn_text_placeholder'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+
+ section_redirect:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+
+ allow_redirect:
+ label: 'janvince.smallcontactform::lang.settings.redirect.allow_redirect'
+ comment: 'janvince.smallcontactform::lang.settings.redirect.allow_redirect_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+
+ redirect_url:
+ label: 'janvince.smallcontactform::lang.settings.redirect.redirect_url'
+ comment: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_placeholder'
+ span: full
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_redirect
+ condition: checked
+
+
+ redirect_url_external:
+ label: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_external'
+ comment: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_external_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_redirect
+ condition: checked
+
+
+ ## FORM FIELDS
+
+ form_fields:
+ prompt: 'janvince.smallcontactform::lang.settings.form_fields.prompt'
+ span: full
+ type: repeater
+ style: collapsed
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form_fields'
+ form:
+ fields:
+ name:
+ span: full
+ label: 'janvince.smallcontactform::lang.settings.form_fields.name'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.name_comment'
+ type: text
+ required: true
+ # cssClass: 'bg-info'
+ type:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.type'
+ type: dropdown
+ required: true
+ cssClass: field-indent
+ label:
+ span: right
+ label: 'janvince.smallcontactform::lang.settings.form_fields.label'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.label_placeholder'
+ type: text
+ cssClass: field-indent
+
+ field_values:
+ # label: 'janvince.smallcontactform::lang.settings.form_fields.validation'
+ prompt: 'janvince.smallcontactform::lang.settings.form_fields.add_values_prompt'
+ span: full
+ type: repeater
+ style: collapsed
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: type
+ condition: value[dropdown]
+ form:
+ fields:
+ field_value_id:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.field_value_id'
+ type: text
+ field_value_content:
+ span: right
+ label: 'janvince.smallcontactform::lang.settings.form_fields.field_value_content'
+ type: text
+
+ field_custom_code:
+ span: full
+ label: 'janvince.smallcontactform::lang.settings.form_fields.custom_code'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.custom_code_comment'
+ type: codeeditor
+ size: large
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: type
+ condition: value[custom_code]
+
+ field_custom_code_twig:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.custom_code_twig'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.custom_code_twig_comment'
+ type: checkbox
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: type
+ condition: value[custom_code]
+
+ field_custom_code_line:
+ span: full
+ type: section
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: type
+ condition: value[custom_code]
+
+ field_custom_content:
+ span: full
+ label: 'janvince.smallcontactform::lang.settings.form_fields.custom_content'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.custom_content_comment'
+ type: richeditor
+ size: large
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: type
+ condition: value[custom_content]
+
+ field_custom_content_section:
+ span: full
+ type: section
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: type
+ condition: value[custom_content]
+
+ field_styling:
+ label: 'janvince.smallcontactform::lang.settings.form_fields.field_styling'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.field_styling_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form_fields'
+ cssClass: field-indent
+
+ autofocus:
+ label: 'janvince.smallcontactform::lang.settings.form_fields.autofocus'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.autofocus_comment'
+ span: right
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.form_fields'
+ cssClass: field-indent
+
+ wrapper_css:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.wrapper_css'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.wrapper_css_placeholder'
+ type: text
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: field_styling
+ condition: checked
+
+ label_css:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.label_css'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.label_css_placeholder'
+ type: text
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: field_styling
+ condition: checked
+
+ field_css:
+ span: right
+ label: 'janvince.smallcontactform::lang.settings.form_fields.field_css'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.field_css_placeholder'
+ type: text
+ trigger:
+ action: show
+ field: field_styling
+ condition: checked
+
+ section_validation:
+ type: section
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: field_styling
+ condition: checked
+
+
+ field_validation:
+ label: 'janvince.smallcontactform::lang.settings.form_fields.field_validation'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.field_validation_comment'
+ span: full
+ type: checkbox
+ cssClass: field-indent
+
+ validation:
+ # label: 'janvince.smallcontactform::lang.settings.form_fields.validation'
+ prompt: 'janvince.smallcontactform::lang.settings.form_fields.validation_prompt'
+ span: full
+ type: repeater
+ style: collapsed
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: field_validation
+ condition: checked
+ form:
+ fields:
+ validation_type:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.validation'
+ type: dropdown
+ emptyOption: 'janvince.smallcontactform::lang.settings.form_field_validation.select'
+ validation_error:
+ span: right
+ label: 'janvince.smallcontactform::lang.settings.form_fields.validation_error'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.validation_error_placeholder'
+ type: text
+ validation_custom_type:
+ span: left
+ label: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_type'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_type_placeholder'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_type_comment'
+ commentHtml: true
+ type: text
+ trigger:
+ action: show
+ field: validation_type
+ condition: value[custom]
+ validation_custom_pattern:
+ span: right
+ label: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_pattern'
+ placeholder: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_pattern_placeholder'
+ comment: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_pattern_comment'
+ type: text
+ trigger:
+ action: show
+ field: validation_type
+ condition: value[custom]
+
+
+ ## MAPPING
+
+ autoreply_email_field:
+ label: 'janvince.smallcontactform::lang.settings.email.autoreply_email_field'
+ commentHtml: true
+ comment: 'janvince.smallcontactform::lang.settings.email.autoreply_email_field_comment'
+ span: left
+ type: dropdown
+ tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
+ emptyOption: 'janvince.smallcontactform::lang.settings.email.autoreply_email_field_empty_option'
+
+ partial_mapping:
+ type: partial
+ path: $/janvince/smallcontactform/models/settings/_mapping_help.htm
+ span: right
+ tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
+
+ autoreply_name_field:
+ label: 'janvince.smallcontactform::lang.settings.email.autoreply_name_field'
+ commentHtml: true
+ comment: 'janvince.smallcontactform::lang.settings.email.autoreply_name_field_comment'
+ span: left
+ type: dropdown
+ tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
+ emptyOption: 'janvince.smallcontactform::lang.settings.email.autoreply_name_field_empty_option'
+
+ autoreply_message_field:
+ label: 'janvince.smallcontactform::lang.settings.email.autoreply_message_field'
+ commentHtml: true
+ comment: 'janvince.smallcontactform::lang.settings.email.autoreply_message_field_comment'
+ span: left
+ type: dropdown
+ tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
+ emptyOption: 'janvince.smallcontactform::lang.settings.email.autoreply_message_field_empty_option'
+
+ partial_mapping_warning:
+ type: partial
+ path: $/janvince/smallcontactform/models/settings/_mapping_warning.htm
+ span: right
+ tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
+
+
+ ## ANTISPAM
+
+ add_google_recaptcha:
+ label: 'janvince.smallcontactform::lang.settings.antispam.add_google_recaptcha'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.add_google_recaptcha_comment'
+ commentHtml: true
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+
+ google_recaptcha_version:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_version'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_version_comment'
+ commentHtml: true
+ span: left
+ type: dropdown
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ options:
+ v2checkbox: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_versions.v2checkbox'
+ v2invisible: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_versions.v2invisible'
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ google_recaptcha_site_key:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_site_key'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_site_key_comment'
+ commentHtml: true
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ google_recaptcha_secret_key:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_secret_key'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_secret_key_comment'
+ span: right
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ google_recaptcha_error_msg:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg_placeholder'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ google_recaptcha_wrapper_css:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css_placeholder'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ google_recaptcha_scripts_allow:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_scripts_allow'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_scripts_allow_comment'
+ span: left
+ type: checkbox
+ default: false
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ google_recaptcha_locale_allow:
+ label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_locale_allow'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_locale_allow_comment'
+ span: left
+ type: checkbox
+ default: false
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ section_google_recaptcha:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ trigger:
+ action: show
+ field: add_google_recaptcha
+ condition: checked
+
+ section_disabled_extensions:
+ type: section
+ label: 'janvince.smallcontactform::lang.settings.antispam.disabled_extensions'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.disabled_extensions_comment'
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ trigger:
+ action: show
+ field: privacy_disable_messages_saving
+ condition: checked
+
+ add_antispam:
+ label: 'janvince.smallcontactform::lang.settings.antispam.add_antispam'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.add_antispam_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+
+ antispam_delay:
+ label: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_placeholder'
+ span: left
+ type: number
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_antispam
+ condition: checked
+
+ antispam_delay_error_msg:
+ label: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg_placeholder'
+ span: right
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_antispam
+ condition: checked
+
+
+ antispam_label:
+ label: 'janvince.smallcontactform::lang.settings.antispam.antispam_label'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_label_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_label_placeholder'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_antispam
+ condition: checked
+
+ antispam_error_msg:
+ label: 'janvince.smallcontactform::lang.settings.antispam.antispam_error_msg'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_error_msg_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_error_msg_placeholder'
+ span: right
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_antispam
+ condition: checked
+
+ section_ip_protection:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ trigger:
+ action: show
+ field: add_antispam
+ condition: checked
+
+ add_ip_protection:
+ label: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ trigger:
+ action: disable
+ field: privacy_disable_messages_saving
+ condition: checked
+
+ add_ip_protection_count:
+ label: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count_placeholder'
+ span: left
+ type: number
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_ip_protection
+ condition: checked
+
+ add_ip_protection_error_too_many_submits:
+ label: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits'
+ comment: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits_comment'
+ placeholder: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits_placeholder'
+ span: right
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: add_ip_protection
+ condition: checked
+
+
+
+ ## EMAILS
+
+ allow_email_queue:
+ label: 'janvince.smallcontactform::lang.settings.email.allow_email_queue'
+ comment: 'janvince.smallcontactform::lang.settings.email.allow_email_queue_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+
+ section_autoreply:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ trigger:
+ action: show
+ field: allow_autoreply
+ condition: checked
+
+ allow_autoreply:
+ label: 'janvince.smallcontactform::lang.settings.email.allow_autoreply'
+ comment: 'janvince.smallcontactform::lang.settings.email.allow_autoreply_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+
+ email_address_from:
+ label: 'janvince.smallcontactform::lang.settings.email.address_from'
+ span: left
+ type: text
+ placeholder: 'janvince.smallcontactform::lang.settings.email.address_from_placeholder'
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_autoreply
+ condition: checked
+
+ email_address_from_name:
+ label: 'janvince.smallcontactform::lang.settings.email.address_from_name'
+ span: right
+ type: text
+ placeholder: 'janvince.smallcontactform::lang.settings.email.address_from_name_placeholder'
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_autoreply
+ condition: checked
+
+ email_address_replyto:
+ label: 'janvince.smallcontactform::lang.settings.email.address_replyto'
+ span: left
+ type: text
+ placeholder: 'janvince.smallcontactform::lang.settings.email.address_from_placeholder'
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_autoreply
+ condition: checked
+
+ email_subject:
+ label: 'janvince.smallcontactform::lang.settings.email.subject'
+ comment: 'janvince.smallcontactform::lang.settings.email.subject_comment'
+ span: full
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_autoreply
+ condition: checked
+
+ email_template:
+ label: 'janvince.smallcontactform::lang.settings.email.template'
+ comment: 'janvince.smallcontactform::lang.settings.email.template_comment'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_autoreply
+ condition: checked
+
+ section_notify:
+ type: section
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ trigger:
+ action: show
+ field: allow_notifications
+ condition: checked
+
+
+ allow_notifications:
+ label: 'janvince.smallcontactform::lang.settings.email.allow_notifications'
+ comment: 'janvince.smallcontactform::lang.settings.email.allow_notifications_comment'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+
+ notification_address_from_form:
+ label: 'janvince.smallcontactform::lang.settings.email.notification_address_from_form'
+ comment: 'janvince.smallcontactform::lang.settings.email.notification_address_from_form_comment'
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_notifications
+ condition: checked
+
+ notification_address_to:
+ label: 'janvince.smallcontactform::lang.settings.email.notification_address_to'
+ comment: 'janvince.smallcontactform::lang.settings.email.notification_address_to_comment'
+ span: left
+ type: text
+ placeholder: 'janvince.smallcontactform::lang.settings.email.notification_address_to_placeholder'
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_notifications
+ condition: checked
+
+ notification_template:
+ label: 'janvince.smallcontactform::lang.settings.email.notification_template'
+ comment: 'janvince.smallcontactform::lang.settings.email.notification_template_comment'
+ span: left
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.email'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: allow_notifications
+ condition: checked
+
+
+ ## Google Analytics
+ section_ga_events:
+ type: section
+ label: 'janvince.smallcontactform::lang.settings.sections.ga_events'
+ tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
+
+ ga_success_event_allow:
+ label: 'janvince.smallcontactform::lang.settings.ga.ga_success_event_allow'
+ span: left
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
+
+ ga_success_event_category:
+ label: 'janvince.smallcontactform::lang.settings.form_fields.event_category'
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: ga_success_event_allow
+ condition: checked
+
+ ga_success_event_action:
+ label: 'janvince.smallcontactform::lang.settings.form_fields.event_action'
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: ga_success_event_allow
+ condition: checked
+
+ ga_success_event_label:
+ label: 'janvince.smallcontactform::lang.settings.form_fields.event_label'
+ type: text
+ tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
+ cssClass: field-indent
+ trigger:
+ action: show
+ field: ga_success_event_allow
+ condition: checked
+
+
+ ## PRIVACY
+
+ privacy_disable_messages_saving:
+ label: 'janvince.smallcontactform::lang.settings.privacy.disable_messages_saving'
+ comment: 'janvince.smallcontactform::lang.settings.privacy.disable_messages_saving_comment'
+ commentHtml: true
+ span: full
+ type: checkbox
+ tab: 'janvince.smallcontactform::lang.settings.tabs.privacy'
+
+ privacy_disable_messages_saving_section:
+ comment: 'janvince.smallcontactform::lang.settings.privacy.disable_messages_saving_comment_section'
+ commentHtml: true
+ span: full
+ type: section
+ cssClass: field-indent
+ tab: 'janvince.smallcontactform::lang.settings.tabs.privacy'
+ trigger:
+ action: show
+ field: privacy_disable_messages_saving
+ condition: checked
diff --git a/plugins/janvince/smallcontactform/reportwidgets/Messages.php b/plugins/janvince/smallcontactform/reportwidgets/Messages.php
new file mode 100644
index 000000000..4deced130
--- /dev/null
+++ b/plugins/janvince/smallcontactform/reportwidgets/Messages.php
@@ -0,0 +1,27 @@
+makePartial('messages');
+ }
+
+ public function getRecordsStats($value){
+
+ $controller = new MessagesController;
+
+ return $controller->getRecordsStats($value);
+
+ }
+
+}
diff --git a/plugins/janvince/smallcontactform/reportwidgets/NewMessage.php b/plugins/janvince/smallcontactform/reportwidgets/NewMessage.php
new file mode 100644
index 000000000..9783b63f2
--- /dev/null
+++ b/plugins/janvince/smallcontactform/reportwidgets/NewMessage.php
@@ -0,0 +1,27 @@
+makePartial('newmessage');
+ }
+
+ public function getRecordsStats($value){
+
+ $controller = new MessagesController;
+
+ return $controller->getRecordsStats($value);
+
+ }
+
+}
diff --git a/plugins/janvince/smallcontactform/reportwidgets/messages/partials/_messages.htm b/plugins/janvince/smallcontactform/reportwidgets/messages/partials/_messages.htm
new file mode 100644
index 000000000..4f4927102
--- /dev/null
+++ b/plugins/janvince/smallcontactform/reportwidgets/messages/partials/_messages.htm
@@ -0,0 +1,17 @@
+
diff --git a/plugins/janvince/smallcontactform/reportwidgets/newmessage/partials/_newmessage.htm b/plugins/janvince/smallcontactform/reportwidgets/newmessage/partials/_newmessage.htm
new file mode 100644
index 000000000..ed7e534a7
--- /dev/null
+++ b/plugins/janvince/smallcontactform/reportwidgets/newmessage/partials/_newmessage.htm
@@ -0,0 +1,13 @@
+
diff --git a/plugins/janvince/smallcontactform/updates/scf_tables.php b/plugins/janvince/smallcontactform/updates/scf_tables.php
new file mode 100644
index 000000000..a5e6579f5
--- /dev/null
+++ b/plugins/janvince/smallcontactform/updates/scf_tables.php
@@ -0,0 +1,31 @@
+engine = 'InnoDB';
+ $table->increments('id');
+ $table->text('name')->nullable();
+ $table->text('email')->nullable();
+ $table->text('message')->nullable();
+ $table->text('form_data')->nullable();
+ $table->boolean('new_message')->default(1);
+ $table->timestamps();
+ });
+
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('janvince_smallcontactform_messages');
+ }
+}
diff --git a/plugins/janvince/smallcontactform/updates/scf_tables_02.php b/plugins/janvince/smallcontactform/updates/scf_tables_02.php
new file mode 100644
index 000000000..2ea165e37
--- /dev/null
+++ b/plugins/janvince/smallcontactform/updates/scf_tables_02.php
@@ -0,0 +1,32 @@
+string('remote_ip')->nullable();
+ $table->index('remote_ip');
+ });
+
+ }
+
+ public function down()
+ {
+ if (Schema::hasColumn('janvince_smallcontactform_messages', 'remote_ip')) {
+
+ Schema::table('janvince_smallcontactform_messages', function($table)
+ {
+ $table->dropColumn('remote_ip');
+ });
+
+ }
+ }
+}
diff --git a/plugins/janvince/smallcontactform/updates/scf_tables_03.php b/plugins/janvince/smallcontactform/updates/scf_tables_03.php
new file mode 100644
index 000000000..56d24233a
--- /dev/null
+++ b/plugins/janvince/smallcontactform/updates/scf_tables_03.php
@@ -0,0 +1,40 @@
+text('form_description')->nullable();
+ $table->string('form_alias')->nullable();
+ });
+
+ }
+
+ public function down()
+ {
+ if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description')) {
+
+ Schema::table('janvince_smallcontactform_messages', function($table)
+ {
+ $table->dropColumn('form_description');
+ });
+
+ }
+ if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_alias')) {
+
+ Schema::table('janvince_smallcontactform_messages', function($table)
+ {
+ $table->dropColumn('form_alias');
+ });
+
+ }
+ }
+}
diff --git a/plugins/janvince/smallcontactform/updates/scf_tables_04.php b/plugins/janvince/smallcontactform/updates/scf_tables_04.php
new file mode 100644
index 000000000..a2b0560e4
--- /dev/null
+++ b/plugins/janvince/smallcontactform/updates/scf_tables_04.php
@@ -0,0 +1,31 @@
+text('url')->nullable();
+ });
+
+ }
+
+ public function down()
+ {
+ if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description')) {
+
+ Schema::table('janvince_smallcontactform_messages', function($table)
+ {
+ $table->dropColumn('url');
+ });
+
+ }
+ }
+}
diff --git a/plugins/janvince/smallcontactform/updates/scf_tables_05.php b/plugins/janvince/smallcontactform/updates/scf_tables_05.php
new file mode 100644
index 000000000..3e06a2c8f
--- /dev/null
+++ b/plugins/janvince/smallcontactform/updates/scf_tables_05.php
@@ -0,0 +1,31 @@
+text('url', 2000)->nullable()->change();
+ });
+ }
+ }
+
+ public function down()
+ {
+ if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description'))
+ {
+ Schema::table('janvince_smallcontactform_messages', function($table)
+ {
+ $table->text('url')->nullable()->change();
+ });
+ }
+ }
+}
diff --git a/plugins/janvince/smallcontactform/updates/version.yaml b/plugins/janvince/smallcontactform/updates/version.yaml
new file mode 100644
index 000000000..73c989806
--- /dev/null
+++ b/plugins/janvince/smallcontactform/updates/version.yaml
@@ -0,0 +1,244 @@
+1.0.0:
+ - "First version of Small Contact Form plugin"
+ - scf_tables.php
+1.0.1:
+ - Fix form hiding after successful send
+ - Fix in README.md
+1.0.2:
+ - Fix some typos and add LICENCE file (thanks Szabó Gergő)
+1.1.0:
+ - Added function to delete records in Messages list
+ - Added permission to delete records
+1.2.0:
+ - Added dashboard report widgets (Stats and New messages)
+1.2.1:
+ - Mail templates now render values with {{ values|raw }}
+1.2.2:
+ - Mail templates convert new lines to with {{ values|raw|nl2br }}
+1.2.3:
+ - Fields mapping moved to separate tab *Columns mapping*
+1.2.4:
+ - Updated README.md with assets usage example
+1.2.5:
+ - Added IP protection function (limit too many submits from one IP address)
+ - And Messages list column to show senders IP address (invisible by default)
+ - scf_tables_02.php
+1.2.6:
+ - Fixed IP protection error message
+1.2.7:
+ - Changed remote_ip column type to string
+1.2.8:
+ - Added option to use placeholders instead of labels
+1.3.0:
+ - Added translation support for Rainlab Translate plugin
+ - Fixed some typos
+1.3.1:
+ - Added default value for getTranslated() method
+1.3.2:
+ - Added custom send button wrapper class
+1.4.0:
+ - Added redirect option after successful submit (internal and external URL)
+1.4.1:
+ - Minor UI fix (thanks Szabó Gergő)
+1.4.2:
+ - Added support for default translated mail templates (Czech and English for now)
+1.4.3:
+ - Fixed translation of mail templates description in Settings > Mail templates
+1.4.4:
+ - Fixed array of enabledLocales
+1.4.5:
+ - Fixed email template check
+ - Added default EN locale to enabled locales array
+1.4.6:
+ - Removed field type restriction for Fields mapping
+1.4.7:
+ - Removed hardcoded date format for created_at column in messages list, updated README and added hungarian language (thanks Szabó Gergő for all this)
+1.4.8:
+ - Changes to allow multiple use of contact form (form and message blocks has now unique IDs)
+ - Added checkbox field type
+ - Scoreboard last message time format (thanks Szabó Gergő)
+1.4.9:
+ - Added scoreboard button to quickly open form settings
+1.4.10:
+ - Fixed typo in lang filename
+1.4.11:
+ - Added "fieldsDetails" array to all email templates to have access to field labels, types and more
+ - Updated default autoreply mail templates to include fieldsDetail array
+ - Added function to detect non-defined fields in sent data
+ - Updated README.md file
+1.5.0:
+ - Added some component hacking options (override autoreply and notification emails and template, disable fields)
+ - Fixed some typos
+ - Updated README.md file
+1.5.1:
+ - Fixed flash message visibility when where are some errors
+1.5.2:
+ - Fixed flash error for IP protection visibility
+1.5.3:
+ - Added option for notification emails to have FROM address set from contact form email field
+1.5.4:
+ - Added option to mark selected messages as read
+1.5.5:
+ - Changed JSON type for repeater DB column
+1.5.6:
+ - Removed value attribute in textarea field
+1.5.7:
+ - Added component alias to id attributes for multi-form usage
+1.5.8:
+ - Fixed typo in lang files
+1.5.9:
+ - Added direct link to messages list from dashboard widget
+1.6.0:
+ - Added Google reCAPTCHA validation
+1.6.1:
+ - Changed All messages large indicator to New messages in scoreboard
+1.6.2:
+ - Removed reCAPTCHA hard coded locale string (thx kuzyk). Added settings option to allow locale detection.
+1.7.0:
+ - Added option to specify custom CSS class
+1.7.1:
+ - Fixed 'text_preview' list field type truncate function (thx kuzyk)
+1.7.2:
+ - Changed count() to mb_strlen() function in custom list type definition
+1.8.0:
+ - Added option to disable built-in browser form validation. Added class 'is-invalid' for fields with error (as used Bootstrap 4).
+1.9.0:
+ - Form registered as Page snippet to be used in Rainlab.Page content (thx BtzLeon)
+1.9.1:
+ - REPLY TO address is now used for notification email by default. You can still force FROM address to be used (but this is not supported by all email systems!).
+1.9.2:
+ - Fix problem when ReCaptcha field was logged as an undefined field to system log (thx LukeTowers)
+1.9.3:
+ - Fixed label 'for' attribute to point to input ID (as required by specification)
+1.10.0:
+ - Added form component hacks group (now only for disabling notification emails, more will come)
+1.11.0:
+ - Added form fields alias and description (can be used to distinquish between more forms or to save extra data). More info in README file.
+ - scf_tables_03.php
+1.11.1:
+ - Added form description field to message preview
+1.12.0:
+ - Added Russian translation (thank Dinver)
+1.12.1:
+ - Chanded input custom list column type for switch to prevent interaction with toolbar JS
+1.13.1:
+ - Added form_alias and form_description variables to email (notification and autoreply) templates
+1.13.2:
+ - Disabled placeholder attribute for checkbox
+1.14.0:
+ - Added option to export messages list
+1.14.1:
+ - Added permissions to export messages list
+1.15.0:
+ - Added Privacy tab and new option to disable sent messages saving
+1.15.1:
+ - Fixed settings fields trigger
+1.15.2:
+ - Fixed default values for recaptcha settings to false
+1.15.3:
+ - Allowed combination of disabled messages saving and allowed passive antispam
+1.16.0:
+ - Added option to have more than one notification email address
+1.16.1:
+ - Fixed missing form data in autoreply templates. Updated default autoreply messages.
+1.16.2:
+ - Updated hungarian translation (thx gergo85)
+1.16.3:
+ - Fixed checkbox validation and validation state
+1.17.0:
+ - Added Slovak translation (thx vosco88)
+1.18.0:
+ - Added French translations (thx FelixINX)
+1.19.0:
+ - Added custom validation fields (thanks petr-vytlacil for help)
+1.20.0:
+ - Added dropdown field type
+1.21.0:
+ - Form fields repeater is now translatable
+1.22.0:
+ - Fixed multiple flash messages shown
+1.23.0:
+ - When placeholders are used, labels are now still present and only hidden by style attribute
+1.24.0:
+ - Added option to set custom reCaptcha wrapper CSS class
+1.25.0:
+ - Added polish (thanks Magiczne) and spanish (thanks codibit) translations
+1.30.0:
+ - Added invisible reCaptcha
+1.30.1:
+ - Fixed reCaptcha scripts load
+1.31.2:
+ - Fixed AJAX redirect when validation error (thanks zlobec)
+1.31.3:
+ - Fixed reCaptcha checkbox version not showing up on older installations
+1.31.4:
+ - Fixed unnecessary refresh after Ajax send (thanks cregx)
+1.32.0:
+ - Added all settings overrides as regular component properties (can be used to override some form settings in multi-form setup)
+ - Updated documentation
+1.32.1:
+ - Fixed test on empty values if some fields are disabled
+1.40.0:
+ - Added validation rule custom_not_regex (inverse of default regex validation rule)
+ - Added form container with ID and action attribute with this hash URL (to automatically jump to form after nonAJAX send or refresh)
+1.40.1:
+ - Fixed notification From name to be correctly set from component properties (thanks @pavsid)
+1.41.0:
+ - Added component redirect properties and allow dynamic redirect URL as a component markup parameter. More info in README file.
+ - Removed hard coded form hash URL (#scf-[form-alias]) as this can be now easily added with redirect options.
+1.41.1:
+ - Set redirect URL property default value to null.
+1.42.0:
+ - Added Google Analytics events after form is successfully sent
+ - Do not populate redirection field code when redirection is not allowed
+1.43.0: !!! Rewritten component partials. No action is needed if you use plugin as is. But if you override component partials test them before update!
+1.44.0:
+ - Added component properties to override notification and autoreply email subject (with support for Twig variables)
+1.45.0:
+ - Added German translation (thanks NiklasDah)
+1.46.0:
+ - Fixed backend validation for reCaptcha invisible
+1.47.0:
+ - Added Custom code and Custom content field types
+ - Updated README
+1.47.1:
+ - Fixed typo in README (wrong component redirection parameter name)
+1.47.2:
+ - Fixed checkbox validation (thanks Chocofede)
+1.47.3:
+ - Removed unnecessarry name attribute from custom_content field
+1.47.4:
+ - Fixed typo in field attributes
+1.48.0:
+ - Added option to set ReplyTo email address for autoreply emails
+1.48.1:
+ - Fixed autoreply email addresses checks
+1.48.2:
+ - Fixed empty ReplyTo field error
+1.48.3:
+ - Fixed getFieldHtmlCode method (thanks sdlb)
+1.49.0:
+ - Fixed missing description and redirect url after AJAX calls
+ - Fields forms in backend are now collapsed by default for better visibility
+1.50.0:
+ - Auto store form request URL in DB (is available also in Mail templates)
+ - scf_tables_04.php
+1.50.1:
+ - Removed unnecessarry debug log
+1.51.0:
+ - Added support for (one or many) file uploads
+ - Fixed AJAX validation
+ - Updated README
+1.51.1:
+ - Removed uploads array from default fields sent to autoreply template
+1.51.2:
+ - Fixed uploads field in email messages
+1.51.3:
+ - Changed size of database column url (thanks zlobec)
+ - scf_tables_05.php
+1.51.4:
+ - Fixed passive antispam delay validation
+1.52.0:
+ - Changed reCaptcha validation to work with allow_url_fopen disabled
+1.52.1:
+ - Fixed project git files
\ No newline at end of file
diff --git a/plugins/janvince/smallcontactform/vendor/autoload.php b/plugins/janvince/smallcontactform/vendor/autoload.php
new file mode 100644
index 000000000..619c9a4ac
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/autoload.php
@@ -0,0 +1,7 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see http://www.php-fig.org/psr/psr-0/
+ * @see http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ // PSR-4
+ private $prefixLengthsPsr4 = array();
+ private $prefixDirsPsr4 = array();
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ private $prefixesPsr0 = array();
+ private $fallbackDirsPsr0 = array();
+
+ private $useIncludePath = false;
+ private $classMap = array();
+ private $classMapAuthoritative = false;
+ private $missingClasses = array();
+ private $apcuPrefix;
+
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', $this->prefixesPsr0);
+ }
+
+ return array();
+ }
+
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 base directories
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return bool|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/plugins/janvince/smallcontactform/vendor/composer/LICENSE b/plugins/janvince/smallcontactform/vendor/composer/LICENSE
new file mode 100644
index 000000000..f27399a04
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+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.
+
diff --git a/plugins/janvince/smallcontactform/vendor/composer/autoload_classmap.php b/plugins/janvince/smallcontactform/vendor/composer/autoload_classmap.php
new file mode 100644
index 000000000..7a91153b0
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/composer/autoload_classmap.php
@@ -0,0 +1,9 @@
+ array($vendorDir . '/google/recaptcha/src/ReCaptcha'),
+ 'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'),
+);
diff --git a/plugins/janvince/smallcontactform/vendor/composer/autoload_real.php b/plugins/janvince/smallcontactform/vendor/composer/autoload_real.php
new file mode 100644
index 000000000..90fde5cee
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/composer/autoload_real.php
@@ -0,0 +1,52 @@
+= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+ if ($useStaticLoader) {
+ require_once __DIR__ . '/autoload_static.php';
+
+ call_user_func(\Composer\Autoload\ComposerStaticInit27d394c7526e9a74451bd4dc314fca57::getInitializer($loader));
+ } else {
+ $map = require __DIR__ . '/autoload_namespaces.php';
+ foreach ($map as $namespace => $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $map = require __DIR__ . '/autoload_psr4.php';
+ foreach ($map as $namespace => $path) {
+ $loader->setPsr4($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+ }
+
+ $loader->register(true);
+
+ return $loader;
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/composer/autoload_static.php b/plugins/janvince/smallcontactform/vendor/composer/autoload_static.php
new file mode 100644
index 000000000..bfca0545a
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/composer/autoload_static.php
@@ -0,0 +1,39 @@
+
+ array (
+ 'ReCaptcha\\' => 10,
+ ),
+ 'C' =>
+ array (
+ 'Composer\\Installers\\' => 20,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'ReCaptcha\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/google/recaptcha/src/ReCaptcha',
+ ),
+ 'Composer\\Installers\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers',
+ ),
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInit27d394c7526e9a74451bd4dc314fca57::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit27d394c7526e9a74451bd4dc314fca57::$prefixDirsPsr4;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/composer/installed.json b/plugins/janvince/smallcontactform/vendor/composer/installed.json
new file mode 100644
index 000000000..3d0eb6883
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/composer/installed.json
@@ -0,0 +1,184 @@
+[
+ {
+ "name": "composer/installers",
+ "version": "dev-main",
+ "version_normalized": "dev-main",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/installers.git",
+ "reference": "1b94b414035400a8be5c694048a0e711a86b1080"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/installers/zipball/1b94b414035400a8be5c694048a0e711a86b1080",
+ "reference": "1b94b414035400a8be5c694048a0e711a86b1080",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0"
+ },
+ "replace": {
+ "roundcube/plugin-installer": "*",
+ "shama/baton": "*"
+ },
+ "require-dev": {
+ "composer/composer": "1.6.* || ^2.0",
+ "composer/semver": "^1 || ^3",
+ "phpstan/phpstan": "^0.12.55",
+ "phpstan/phpstan-phpunit": "^0.12.16",
+ "symfony/phpunit-bridge": "^4.2 || ^5",
+ "symfony/process": "^2.3"
+ },
+ "time": "2021-03-08T12:02:54+00:00",
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Composer\\Installers\\Plugin",
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "installation-source": "source",
+ "autoload": {
+ "psr-4": {
+ "Composer\\Installers\\": "src/Composer/Installers"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kyle Robinson Young",
+ "email": "kyle@dontkry.com",
+ "homepage": "https://github.com/shama"
+ }
+ ],
+ "description": "A multi-framework Composer library installer",
+ "homepage": "https://composer.github.io/installers/",
+ "keywords": [
+ "Craft",
+ "Dolibarr",
+ "Eliasis",
+ "Hurad",
+ "ImageCMS",
+ "Kanboard",
+ "Lan Management System",
+ "MODX Evo",
+ "MantisBT",
+ "Mautic",
+ "Maya",
+ "OXID",
+ "Plentymarkets",
+ "Porto",
+ "RadPHP",
+ "SMF",
+ "Starbug",
+ "Thelia",
+ "Whmcs",
+ "WolfCMS",
+ "agl",
+ "aimeos",
+ "annotatecms",
+ "attogram",
+ "bitrix",
+ "cakephp",
+ "chef",
+ "cockpit",
+ "codeigniter",
+ "concrete5",
+ "croogo",
+ "dokuwiki",
+ "drupal",
+ "eZ Platform",
+ "elgg",
+ "expressionengine",
+ "fuelphp",
+ "grav",
+ "installer",
+ "itop",
+ "joomla",
+ "known",
+ "kohana",
+ "laravel",
+ "lavalite",
+ "lithium",
+ "magento",
+ "majima",
+ "mako",
+ "mediawiki",
+ "modulework",
+ "modx",
+ "moodle",
+ "osclass",
+ "phpbb",
+ "piwik",
+ "ppi",
+ "processwire",
+ "puppet",
+ "pxcms",
+ "reindex",
+ "roundcube",
+ "shopware",
+ "silverstripe",
+ "sydes",
+ "sylius",
+ "symfony",
+ "tastyigniter",
+ "typo3",
+ "wordpress",
+ "yawik",
+ "zend",
+ "zikula"
+ ]
+ },
+ {
+ "name": "google/recaptcha",
+ "version": "dev-master",
+ "version_normalized": "9999999-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/google/recaptcha.git",
+ "reference": "f911286ad361c9fba1b422c07f040852c0c193a3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/google/recaptcha/zipball/f911286ad361c9fba1b422c07f040852c0c193a3",
+ "reference": "f911286ad361c9fba1b422c07f040852c0c193a3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.2.20|^2.15",
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11"
+ },
+ "time": "2020-10-01T15:14:41+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "installation-source": "source",
+ "autoload": {
+ "psr-4": {
+ "ReCaptcha\\": "src/ReCaptcha"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
+ "homepage": "https://www.google.com/recaptcha/",
+ "keywords": [
+ "Abuse",
+ "captcha",
+ "recaptcha",
+ "spam"
+ ]
+ }
+]
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/LICENSE b/plugins/janvince/smallcontactform/vendor/google/recaptcha/LICENSE
new file mode 100644
index 000000000..d147b35b3
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2019, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/README.md b/plugins/janvince/smallcontactform/vendor/google/recaptcha/README.md
new file mode 100644
index 000000000..92e8deae7
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/README.md
@@ -0,0 +1,140 @@
+# reCAPTCHA PHP client library
+
+[](https://travis-ci.org/google/recaptcha)
+[](https://coveralls.io/github/google/recaptcha)
+[](https://packagist.org/packages/google/recaptcha)
+[](https://packagist.org/packages/google/recaptcha)
+
+reCAPTCHA is a free CAPTCHA service that protects websites from spam and abuse.
+This is a PHP library that wraps up the server-side verification step required
+to process responses from the reCAPTCHA service. This client supports both v2
+and v3.
+
+- reCAPTCHA: https://www.google.com/recaptcha
+- This repo: https://github.com/google/recaptcha
+- Hosted demo: https://recaptcha-demo.appspot.com/
+- Version: 1.2.4
+- License: BSD, see [LICENSE](LICENSE)
+
+## Installation
+
+### Composer (recommended)
+
+Use [Composer](https://getcomposer.org) to install this library from Packagist:
+[`google/recaptcha`](https://packagist.org/packages/google/recaptcha)
+
+Run the following command from your project directory to add the dependency:
+
+```sh
+composer require google/recaptcha "^1.2"
+```
+
+Alternatively, add the dependency directly to your `composer.json` file:
+
+```json
+"require": {
+ "google/recaptcha": "^1.2"
+}
+```
+
+### Direct download
+
+Download the [ZIP file](https://github.com/google/recaptcha/archive/master.zip)
+and extract into your project. An autoloader script is provided in
+`src/autoload.php` which you can require into your script. For example:
+
+```php
+require_once '/path/to/recaptcha/src/autoload.php';
+$recaptcha = new \ReCaptcha\ReCaptcha($secret);
+```
+
+The classes in the project are structured according to the
+[PSR-4](http://www.php-fig.org/psr/psr-4/) standard, so you can also use your
+own autoloader or require the needed files directly in your code.
+
+## Usage
+
+First obtain the appropriate keys for the type of reCAPTCHA you wish to
+integrate for v2 at https://www.google.com/recaptcha/admin or v3 at
+https://g.co/recaptcha/v3.
+
+Then follow the [integration guide on the developer
+site](https://developers.google.com/recaptcha/intro) to add the reCAPTCHA
+functionality into your frontend.
+
+This library comes in when you need to verify the user's response. On the PHP
+side you need the response from the reCAPTCHA service and secret key from your
+credentials. Instantiate the `ReCaptcha` class with your secret key, specify any
+additional validation rules, and then call `verify()` with the reCAPTCHA
+response and user's IP address. For example:
+
+```php
+setExpectedHostname('recaptcha-demo.appspot.com')
+ ->verify($gRecaptchaResponse, $remoteIp);
+if ($resp->isSuccess()) {
+ // Verified!
+} else {
+ $errors = $resp->getErrorCodes();
+}
+```
+
+The following methods are available:
+
+- `setExpectedHostname($hostname)`: ensures the hostname matches. You must do
+ this if you have disabled "Domain/Package Name Validation" for your
+ credentials.
+- `setExpectedApkPackageName($apkPackageName)`: if you're verifying a response
+ from an Android app. Again, you must do this if you have disabled
+ "Domain/Package Name Validation" for your credentials.
+- `setExpectedAction($action)`: ensures the action matches for the v3 API.
+- `setScoreThreshold($threshold)`: set a score threshold for responses from the
+ v3 API
+- `setChallengeTimeout($timeoutSeconds)`: set a timeout between the user passing
+ the reCAPTCHA and your server processing it.
+
+Each of the `set`\*`()` methods return the `ReCaptcha` instance so you can chain
+them together. For example:
+
+```php
+setExpectedHostname('recaptcha-demo.appspot.com')
+ ->setExpectedAction('homepage')
+ ->setScoreThreshold(0.5)
+ ->verify($gRecaptchaResponse, $remoteIp);
+
+if ($resp->isSuccess()) {
+ // Verified!
+} else {
+ $errors = $resp->getErrorCodes();
+}
+```
+
+You can find the constants for the libraries error codes in the `ReCaptcha`
+class constants, e.g. `ReCaptcha::E_HOSTNAME_MISMATCH`
+
+For more details on usage and structure, see [ARCHITECTURE](ARCHITECTURE.md).
+
+### Examples
+
+You can see examples of each reCAPTCHA type in [examples/](examples/). You can
+run the examples locally by using the Composer script:
+
+```sh
+composer run-script serve-examples
+```
+
+This makes use of the in-built PHP dev server to host the examples at
+http://localhost:8080/
+
+These are also hosted on Google AppEngine Flexible environment at
+https://recaptcha-demo.appspot.com/. This is configured by
+[`app.yaml`](./app.yaml) which you can also use to [deploy to your own AppEngine
+project](https://cloud.google.com/appengine/docs/flexible/php/download).
+
+## Contributing
+
+No one ever has enough engineers, so we're very happy to accept contributions
+via Pull Requests. For details, see [CONTRIBUTING](CONTRIBUTING.md)
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/composer.json b/plugins/janvince/smallcontactform/vendor/google/recaptcha/composer.json
new file mode 100644
index 000000000..ab6b4f1c0
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "google/recaptcha",
+ "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
+ "type": "library",
+ "keywords": ["recaptcha", "captcha", "spam", "abuse"],
+ "homepage": "https://www.google.com/recaptcha/",
+ "license": "BSD-3-Clause",
+ "support": {
+ "forum": "https://groups.google.com/forum/#!forum/recaptcha",
+ "source": "https://github.com/google/recaptcha"
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11",
+ "friendsofphp/php-cs-fixer": "^2.2.20|^2.15",
+ "php-coveralls/php-coveralls": "^2.1"
+ },
+ "autoload": {
+ "psr-4": {
+ "ReCaptcha\\": "src/ReCaptcha"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "scripts": {
+ "lint": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no --dry-run .",
+ "lint-fix": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no .",
+ "test": "vendor/bin/phpunit --colors=always",
+ "serve-examples": "@php -S localhost:8080 -t examples"
+ },
+ "config": {
+ "process-timeout": 0
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php
new file mode 100644
index 000000000..31ec44a07
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/ReCaptcha.php
@@ -0,0 +1,269 @@
+secret = $secret;
+ $this->requestMethod = (is_null($requestMethod)) ? new RequestMethod\Post() : $requestMethod;
+ }
+
+ /**
+ * Calls the reCAPTCHA siteverify API to verify whether the user passes
+ * CAPTCHA test and additionally runs any specified additional checks
+ *
+ * @param string $response The user response token provided by reCAPTCHA, verifying the user on your site.
+ * @param string $remoteIp The end user's IP address.
+ * @return Response Response from the service.
+ */
+ public function verify($response, $remoteIp = null)
+ {
+ // Discard empty solution submissions
+ if (empty($response)) {
+ $recaptchaResponse = new Response(false, array(self::E_MISSING_INPUT_RESPONSE));
+ return $recaptchaResponse;
+ }
+
+ $params = new RequestParameters($this->secret, $response, $remoteIp, self::VERSION);
+ $rawResponse = $this->requestMethod->submit($params);
+ $initialResponse = Response::fromJson($rawResponse);
+ $validationErrors = array();
+
+ if (isset($this->hostname) && strcasecmp($this->hostname, $initialResponse->getHostname()) !== 0) {
+ $validationErrors[] = self::E_HOSTNAME_MISMATCH;
+ }
+
+ if (isset($this->apkPackageName) && strcasecmp($this->apkPackageName, $initialResponse->getApkPackageName()) !== 0) {
+ $validationErrors[] = self::E_APK_PACKAGE_NAME_MISMATCH;
+ }
+
+ if (isset($this->action) && strcasecmp($this->action, $initialResponse->getAction()) !== 0) {
+ $validationErrors[] = self::E_ACTION_MISMATCH;
+ }
+
+ if (isset($this->threshold) && $this->threshold > $initialResponse->getScore()) {
+ $validationErrors[] = self::E_SCORE_THRESHOLD_NOT_MET;
+ }
+
+ if (isset($this->timeoutSeconds)) {
+ $challengeTs = strtotime($initialResponse->getChallengeTs());
+
+ if ($challengeTs > 0 && time() - $challengeTs > $this->timeoutSeconds) {
+ $validationErrors[] = self::E_CHALLENGE_TIMEOUT;
+ }
+ }
+
+ if (empty($validationErrors)) {
+ return $initialResponse;
+ }
+
+ return new Response(
+ false,
+ array_merge($initialResponse->getErrorCodes(), $validationErrors),
+ $initialResponse->getHostname(),
+ $initialResponse->getChallengeTs(),
+ $initialResponse->getApkPackageName(),
+ $initialResponse->getScore(),
+ $initialResponse->getAction()
+ );
+ }
+
+ /**
+ * Provide a hostname to match against in verify()
+ * This should be without a protocol or trailing slash, e.g. www.google.com
+ *
+ * @param string $hostname Expected hostname
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setExpectedHostname($hostname)
+ {
+ $this->hostname = $hostname;
+ return $this;
+ }
+
+ /**
+ * Provide an APK package name to match against in verify()
+ *
+ * @param string $apkPackageName Expected APK package name
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setExpectedApkPackageName($apkPackageName)
+ {
+ $this->apkPackageName = $apkPackageName;
+ return $this;
+ }
+
+ /**
+ * Provide an action to match against in verify()
+ * This should be set per page.
+ *
+ * @param string $action Expected action
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setExpectedAction($action)
+ {
+ $this->action = $action;
+ return $this;
+ }
+
+ /**
+ * Provide a threshold to meet or exceed in verify()
+ * Threshold should be a float between 0 and 1 which will be tested as response >= threshold.
+ *
+ * @param float $threshold Expected threshold
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setScoreThreshold($threshold)
+ {
+ $this->threshold = floatval($threshold);
+ return $this;
+ }
+
+ /**
+ * Provide a timeout in seconds to test against the challenge timestamp in verify()
+ *
+ * @param int $timeoutSeconds Expected hostname
+ * @return ReCaptcha Current instance for fluent interface
+ */
+ public function setChallengeTimeout($timeoutSeconds)
+ {
+ $this->timeoutSeconds = $timeoutSeconds;
+ return $this;
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php
new file mode 100644
index 000000000..0a2a6716e
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod.php
@@ -0,0 +1,50 @@
+curl = (is_null($curl)) ? new Curl() : $curl;
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ }
+
+ /**
+ * Submit the cURL request with the specified parameters.
+ *
+ * @param RequestParameters $params Request parameters
+ * @return string Body of the reCAPTCHA response
+ */
+ public function submit(RequestParameters $params)
+ {
+ $handle = $this->curl->init($this->siteVerifyUrl);
+
+ $options = array(
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $params->toQueryString(),
+ CURLOPT_HTTPHEADER => array(
+ 'Content-Type: application/x-www-form-urlencoded'
+ ),
+ CURLINFO_HEADER_OUT => false,
+ CURLOPT_HEADER => false,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_SSL_VERIFYPEER => true
+ );
+ $this->curl->setoptArray($handle, $options);
+
+ $response = $this->curl->exec($handle);
+ $this->curl->close($handle);
+
+ if ($response !== false) {
+ return $response;
+ }
+
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php
new file mode 100644
index 000000000..a4ff716fb
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Post.php
@@ -0,0 +1,88 @@
+siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ }
+
+ /**
+ * Submit the POST request with the specified parameters.
+ *
+ * @param RequestParameters $params Request parameters
+ * @return string Body of the reCAPTCHA response
+ */
+ public function submit(RequestParameters $params)
+ {
+ $options = array(
+ 'http' => array(
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => $params->toQueryString(),
+ // Force the peer to validate (not needed in 5.6.0+, but still works)
+ 'verify_peer' => true,
+ ),
+ );
+ $context = stream_context_create($options);
+ $response = file_get_contents($this->siteVerifyUrl, false, $context);
+
+ if ($response !== false) {
+ return $response;
+ }
+
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php
new file mode 100644
index 000000000..236bd5f5d
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/Socket.php
@@ -0,0 +1,112 @@
+handle = fsockopen($hostname, $port, $errno, $errstr, (is_null($timeout) ? ini_get("default_socket_timeout") : $timeout));
+
+ if ($this->handle != false && $errno === 0 && $errstr === '') {
+ return $this->handle;
+ }
+ return false;
+ }
+
+ /**
+ * fwrite
+ *
+ * @see http://php.net/fwrite
+ * @param string $string
+ * @param int $length
+ * @return int | bool
+ */
+ public function fwrite($string, $length = null)
+ {
+ return fwrite($this->handle, $string, (is_null($length) ? strlen($string) : $length));
+ }
+
+ /**
+ * fgets
+ *
+ * @see http://php.net/fgets
+ * @param int $length
+ * @return string
+ */
+ public function fgets($length = null)
+ {
+ return fgets($this->handle, $length);
+ }
+
+ /**
+ * feof
+ *
+ * @see http://php.net/feof
+ * @return bool
+ */
+ public function feof()
+ {
+ return feof($this->handle);
+ }
+
+ /**
+ * fclose
+ *
+ * @see http://php.net/fclose
+ * @return bool
+ */
+ public function fclose()
+ {
+ return fclose($this->handle);
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php
new file mode 100644
index 000000000..464bc28d4
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestMethod/SocketPost.php
@@ -0,0 +1,108 @@
+socket = (is_null($socket)) ? new Socket() : $socket;
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ }
+
+ /**
+ * Submit the POST request with the specified parameters.
+ *
+ * @param RequestParameters $params Request parameters
+ * @return string Body of the reCAPTCHA response
+ */
+ public function submit(RequestParameters $params)
+ {
+ $errno = 0;
+ $errstr = '';
+ $urlParsed = parse_url($this->siteVerifyUrl);
+
+ if (false === $this->socket->fsockopen('ssl://' . $urlParsed['host'], 443, $errno, $errstr, 30)) {
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
+ }
+
+ $content = $params->toQueryString();
+
+ $request = "POST " . $urlParsed['path'] . " HTTP/1.0\r\n";
+ $request .= "Host: " . $urlParsed['host'] . "\r\n";
+ $request .= "Content-Type: application/x-www-form-urlencoded\r\n";
+ $request .= "Content-length: " . strlen($content) . "\r\n";
+ $request .= "Connection: close\r\n\r\n";
+ $request .= $content . "\r\n\r\n";
+
+ $this->socket->fwrite($request);
+ $response = '';
+
+ while (!$this->socket->feof()) {
+ $response .= $this->socket->fgets(4096);
+ }
+
+ $this->socket->fclose();
+
+ if (0 !== strpos($response, 'HTTP/1.0 200 OK')) {
+ return '{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}';
+ }
+
+ $parts = preg_split("#\n\s*\n#Uis", $response);
+
+ return $parts[1];
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php
new file mode 100644
index 000000000..e9ba45354
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/RequestParameters.php
@@ -0,0 +1,111 @@
+secret = $secret;
+ $this->response = $response;
+ $this->remoteIp = $remoteIp;
+ $this->version = $version;
+ }
+
+ /**
+ * Array representation.
+ *
+ * @return array Array formatted parameters.
+ */
+ public function toArray()
+ {
+ $params = array('secret' => $this->secret, 'response' => $this->response);
+
+ if (!is_null($this->remoteIp)) {
+ $params['remoteip'] = $this->remoteIp;
+ }
+
+ if (!is_null($this->version)) {
+ $params['version'] = $this->version;
+ }
+
+ return $params;
+ }
+
+ /**
+ * Query string representation for HTTP request.
+ *
+ * @return string Query string formatted parameters.
+ */
+ public function toQueryString()
+ {
+ return http_build_query($this->toArray(), '', '&');
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/Response.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/Response.php
new file mode 100644
index 000000000..55838c074
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/ReCaptcha/Response.php
@@ -0,0 +1,218 @@
+success = $success;
+ $this->hostname = $hostname;
+ $this->challengeTs = $challengeTs;
+ $this->apkPackageName = $apkPackageName;
+ $this->score = $score;
+ $this->action = $action;
+ $this->errorCodes = $errorCodes;
+ }
+
+ /**
+ * Is success?
+ *
+ * @return boolean
+ */
+ public function isSuccess()
+ {
+ return $this->success;
+ }
+
+ /**
+ * Get error codes.
+ *
+ * @return array
+ */
+ public function getErrorCodes()
+ {
+ return $this->errorCodes;
+ }
+
+ /**
+ * Get hostname.
+ *
+ * @return string
+ */
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ /**
+ * Get challenge timestamp
+ *
+ * @return string
+ */
+ public function getChallengeTs()
+ {
+ return $this->challengeTs;
+ }
+
+ /**
+ * Get APK package name
+ *
+ * @return string
+ */
+ public function getApkPackageName()
+ {
+ return $this->apkPackageName;
+ }
+ /**
+ * Get score
+ *
+ * @return float
+ */
+ public function getScore()
+ {
+ return $this->score;
+ }
+
+ /**
+ * Get action
+ *
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ public function toArray()
+ {
+ return array(
+ 'success' => $this->isSuccess(),
+ 'hostname' => $this->getHostname(),
+ 'challenge_ts' => $this->getChallengeTs(),
+ 'apk_package_name' => $this->getApkPackageName(),
+ 'score' => $this->getScore(),
+ 'action' => $this->getAction(),
+ 'error-codes' => $this->getErrorCodes(),
+ );
+ }
+}
diff --git a/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/autoload.php b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/autoload.php
new file mode 100644
index 000000000..7947a1050
--- /dev/null
+++ b/plugins/janvince/smallcontactform/vendor/google/recaptcha/src/autoload.php
@@ -0,0 +1,69 @@
+
+
+
+
+ {% for field in fieldsDetails %}
+
+ {% if field.label == 'form_description' or field.label == 'form_alias' %}
+ {% else %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+ {{field.label}}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+Best regards,
+
+Contact form
+
+
+==
+
+Hello,
+
+this is a confirmation from Contact form.
+
+{% if fieldsDetails|length %}
+
+
+
+You have sent this:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+Best regards,
+
+Contact form
diff --git a/plugins/janvince/smallcontactform/views/mail/autoreply_cs.htm b/plugins/janvince/smallcontactform/views/mail/autoreply_cs.htm
new file mode 100644
index 000000000..b5abdf1e4
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/autoreply_cs.htm
@@ -0,0 +1,76 @@
+subject = "Potvrzení doručení zprávy z kontaktního formuláře"
+==
+
+Dobrý den,
+
+posíláme potvrzení přijetí vaší zprávy odeslané z kontaktního formuláře.
+
+{% if fields|length %}
+Vyplnil/a jste toto:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+ {% if field.label == 'form_description' or field.label == 'form_alias' %}
+ {% else %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+S pozdravem,
+
+Kontaktní formulář
+
+
+==
+
+Dobrý den,
+
+posíláme potvrzení přijetí vaší zprávy odeslané z kontaktního formuláře.
+
+{% if fields|length %}
+
+
+
+Vyplnil/a jste toto:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+S pozdravem,
+
+Kontaktní formulář
diff --git a/plugins/janvince/smallcontactform/views/mail/autoreply_es.htm b/plugins/janvince/smallcontactform/views/mail/autoreply_es.htm
new file mode 100644
index 000000000..513a64305
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/autoreply_es.htm
@@ -0,0 +1,77 @@
+subject = "Confirmación del formulario de contacto"
+==
+
+Hola,
+
+esto es una confirmación del formulario de contacto.
+
+{% if fieldsDetails|length %}
+Ha enviado lo siguiente:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+ {% if field.label == 'form_description' or field.label == 'form_alias' %}
+ {% else %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+ {{field.label}}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+Le saluda atentamente,
+
+El formulario de contacto
+
+
+==
+
+Hola,
+
+esto es una confirmación del formulario de contacto.
+
+{% if fieldsDetails|length %}
+
+
+
+Ha enviado lo siguiente:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+Le saluda atentamente,
+
+El formulario de contacto
diff --git a/plugins/janvince/smallcontactform/views/mail/autoreply_pl.htm b/plugins/janvince/smallcontactform/views/mail/autoreply_pl.htm
new file mode 100644
index 000000000..561d3247b
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/autoreply_pl.htm
@@ -0,0 +1,77 @@
+subject = "Potwierdzenie wysłania formularza"
+==
+
+Dzień dobry,
+
+to jest potwierdzenie wysłane z formularza kontaktowego.
+
+{% if fieldsDetails|length %}
+Wiadomość, którą wysłałeś:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+ {% if field.label == 'form_description' or field.label == 'form_alias' %}
+ {% else %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+ {{field.label}}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+Z poważaniem,
+
+Formularz kontaktowy
+
+
+==
+
+Dzień dobry,
+
+to jest potwierdzenie wysłane z formularza kontaktowego.
+
+{% if fieldsDetails|length %}
+
+
+
+Wiadomość, którą wysłałeś:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+Z poważaniem,
+
+ Formularz kontaktowy
diff --git a/plugins/janvince/smallcontactform/views/mail/notification.htm b/plugins/janvince/smallcontactform/views/mail/notification.htm
new file mode 100644
index 000000000..681e815dd
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/notification.htm
@@ -0,0 +1,71 @@
+subject = "Contact form notification"
+==
+
+Hello,
+
+this is a notification from a Contact form.
+
+{% if fields|length %}
+Sent form content:
+
+
+
+
+
+ {% for key,value in fields %}
+
+
+ {{ key|upper }}
+ {{ value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+Best regards,
+
+Contact form
+
+
+==
+
+Hello,
+
+this is a notification from a Contact form.
+
+{% if fields|length %}
+
+
+
+Sent form content:
+
+
+
+
+
+ {% for key,value in fields %}
+
+
+ {{ key|upper }}
+ {{ value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+Best regards,
+
+Contact form
diff --git a/plugins/janvince/smallcontactform/views/mail/notification_cs.htm b/plugins/janvince/smallcontactform/views/mail/notification_cs.htm
new file mode 100644
index 000000000..ee5ffdc95
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/notification_cs.htm
@@ -0,0 +1,71 @@
+subject = "Oznámení o odeslání kontaktního formuláře"
+==
+
+Dobrý den,
+
+toto je oznámení o odeslání kontaktního formuláře.
+
+{% if fields|length %}
+Odeslaný obsah:
+
+
+
+
+
+ {% for key,value in fields %}
+
+
+ {{ key|upper }}
+ {{ value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+S pozdravem,
+
+Kontaktní formulář
+
+
+==
+
+Dobrý den,
+
+toto je oznámení o odeslání kontaktního formuláře.
+
+{% if fields|length %}
+
+
+
+Odeslaný obsah:
+
+
+
+
+
+ {% for key,value in fields %}
+
+
+ {{ key|upper }}
+ {{ value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+S pozdravem,
+
+Kontaktní formulář
diff --git a/plugins/janvince/smallcontactform/views/mail/notification_es.htm b/plugins/janvince/smallcontactform/views/mail/notification_es.htm
new file mode 100644
index 000000000..d1732dc55
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/notification_es.htm
@@ -0,0 +1,70 @@
+subject = "Notificación del formulario de contacto"
+==
+
+Hola,
+esto es una notificación de un formulario de contacto.
+
+{% if fields|length %}
+Contenido del formulario enviado:
+
+
+
+
+
+ {% for key,value in fields %}
+
+
+ {{ key|upper }}
+ {{ value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+Le saluda atentamente,
+
+El formulario de contacto
+
+
+==
+
+Hola,
+
+esto es una notificación de un formulario de contacto..
+
+{% if fields|length %}
+
+
+
+Sent form content:
+
+
+
+
+
+ {% for key,value in fields %}
+
+
+ {{ key|upper }}
+ {{ value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+Le saluda atentamente,
+
+El formulario de contacto
diff --git a/plugins/janvince/smallcontactform/views/mail/notification_pl.htm b/plugins/janvince/smallcontactform/views/mail/notification_pl.htm
new file mode 100644
index 000000000..3ed6396ed
--- /dev/null
+++ b/plugins/janvince/smallcontactform/views/mail/notification_pl.htm
@@ -0,0 +1,77 @@
+subject = "Powiadomienie o nowej wiadomości"
+==
+
+Dzień dobry,
+
+to jest powiadomienie wysłane z formularza kontaktowego.
+
+{% if fieldsDetails|length %}
+Otrzymałeś nową wiadomość:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+ {% if field.label == 'form_description' or field.label == 'form_alias' %}
+ {% else %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+ {{field.label}}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+Z poważaniem,
+
+Formularz kontaktowy
+
+
+==
+
+Dzień dobry,
+
+to jest powiadomienie wysłane z formularza kontaktowego.
+
+{% if fieldsDetails|length %}
+
+
+
+Otrzymałeś nową wiadomość:
+
+
+
+
+
+ {% for field in fieldsDetails %}
+
+
+ {{ field.label }}
+ {{ field.value|raw|nl2br }}
+
+
+ {% endfor %}
+
+
+
+
+
+{% endif %}
+
+
+
+Z poważaniem,
+
+ Formularz kontaktowy
diff --git a/plugins/lovata/toolbox/models/Settings.php b/plugins/lovata/toolbox/models/Settings.php
index 4c2c8df55..b14e9b3ac 100644
--- a/plugins/lovata/toolbox/models/Settings.php
+++ b/plugins/lovata/toolbox/models/Settings.php
@@ -10,4 +10,6 @@ class Settings extends CommonSettings
const SETTINGS_CODE = 'lovata_toolbox_settings';
public $settingsCode = 'lovata_toolbox_settings';
+
+ public $translatable = ['address','site_name'];
}
diff --git a/plugins/lovata/toolbox/models/settings/fields.yaml b/plugins/lovata/toolbox/models/settings/fields.yaml
index 7cf9042aa..72e18395b 100644
--- a/plugins/lovata/toolbox/models/settings/fields.yaml
+++ b/plugins/lovata/toolbox/models/settings/fields.yaml
@@ -1,10 +1,43 @@
tabs:
fields:
+ site_name:
+ tab: lovata.toolbox::lang.tab.settings
+ label: Site name
+
+ dollar:
+ tab: lovata.toolbox::lang.tab.settings
+ span: left
+ label: Dollar kurs
+
+ euro:
+ tab: lovata.toolbox::lang.tab.settings
+ span: right
+ label: Euro kurs
+
+ gbp:
+ tab: lovata.toolbox::lang.tab.settings
+ span: left
+ label: GBP kurs
+ phone:
+ tab: lovata.toolbox::lang.tab.settings
+ span: left
+ label: Telefon
+
+ email:
+ tab: lovata.toolbox::lang.tab.settings
+ span: right
+ label: Email
+
+ address:
+ tab: lovata.toolbox::lang.tab.settings
+ label: Adres
+
slug_is_translatable:
tab: lovata.toolbox::lang.tab.settings
span: left
label: lovata.toolbox::lang.settings.slug_is_translatable
type: checkbox
+
queue_on:
tab: lovata.toolbox::lang.tab.mail
span: left
@@ -44,4 +77,4 @@ tabs:
tab: lovata.toolbox::lang.tab.import
span: left
label: lovata.toolbox::lang.field.import_queue_name
- type: text
\ No newline at end of file
+ type: text
diff --git a/plugins/rainlab/pages/.github/ISSUE_TEMPLATE/1_BUG_REPORT.md b/plugins/rainlab/pages/.github/ISSUE_TEMPLATE/1_BUG_REPORT.md
new file mode 100644
index 000000000..5a58d175a
--- /dev/null
+++ b/plugins/rainlab/pages/.github/ISSUE_TEMPLATE/1_BUG_REPORT.md
@@ -0,0 +1,15 @@
+---
+name: "🐛 Bug Report"
+about: 'Report a general Pages Plugin issue'
+labels: 'Status: Review Needed, Type: Unconfirmed Bug'
+---
+
+- OctoberCMS Build: ###
+- RainLab Pages Plugin Version: ###
+- PHP Version:
+
+### Description:
+
+
+### Steps To Reproduce:
+
diff --git a/plugins/rainlab/pages/.github/ISSUE_TEMPLATE/2_GENERAL_SUPPORT.md b/plugins/rainlab/pages/.github/ISSUE_TEMPLATE/2_GENERAL_SUPPORT.md
new file mode 100644
index 000000000..935714f02
--- /dev/null
+++ b/plugins/rainlab/pages/.github/ISSUE_TEMPLATE/2_GENERAL_SUPPORT.md
@@ -0,0 +1,13 @@
+---
+name: "⚠️ General Support"
+about: 'This repository is only for reporting bugs or problems. If you need help using RainLab Pages, see: https://octobercms.com/support'
+---
+
+This repository is only for reporting bugs or problems with the RainLab Pages plugin. If you need support, please use
+the following options:
+
+- Forum: https://octobercms.com/forum
+- Discord: https://discord.com/invite/gEKgwSZ
+- Stack Overflow: https://stackoverflow.com/questions/tagged/octobercms
+
+Thanks!
diff --git a/plugins/rainlab/pages/.gitignore b/plugins/rainlab/pages/.gitignore
new file mode 100644
index 000000000..3d725761b
--- /dev/null
+++ b/plugins/rainlab/pages/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+.idea
\ No newline at end of file
diff --git a/plugins/rainlab/pages/LICENCE.md b/plugins/rainlab/pages/LICENCE.md
new file mode 100644
index 000000000..d68943ee4
--- /dev/null
+++ b/plugins/rainlab/pages/LICENCE.md
@@ -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.
\ No newline at end of file
diff --git a/plugins/rainlab/pages/Plugin.php b/plugins/rainlab/pages/Plugin.php
new file mode 100644
index 000000000..4e36c15fb
--- /dev/null
+++ b/plugins/rainlab/pages/Plugin.php
@@ -0,0 +1,252 @@
+ 'rainlab.pages::lang.plugin.name',
+ 'description' => 'rainlab.pages::lang.plugin.description',
+ 'author' => 'Alexey Bobkov, Samuel Georges',
+ 'icon' => 'icon-files-o',
+ 'homepage' => 'https://github.com/rainlab/pages-plugin'
+ ];
+ }
+
+ public function registerComponents()
+ {
+ return [
+ '\RainLab\Pages\Components\ChildPages' => 'childPages',
+ '\RainLab\Pages\Components\StaticPage' => 'staticPage',
+ '\RainLab\Pages\Components\StaticMenu' => 'staticMenu',
+ '\RainLab\Pages\Components\StaticBreadcrumbs' => 'staticBreadcrumbs'
+ ];
+ }
+
+ public function registerPermissions()
+ {
+ return [
+ 'rainlab.pages.manage_pages' => [
+ 'tab' => 'rainlab.pages::lang.page.tab',
+ 'order' => 200,
+ 'label' => 'rainlab.pages::lang.page.manage_pages'
+ ],
+ 'rainlab.pages.manage_menus' => [
+ 'tab' => 'rainlab.pages::lang.page.tab',
+ 'order' => 200,
+ 'label' => 'rainlab.pages::lang.page.manage_menus'
+ ],
+ 'rainlab.pages.manage_content' => [
+ 'tab' => 'rainlab.pages::lang.page.tab',
+ 'order' => 200,
+ 'label' => 'rainlab.pages::lang.page.manage_content'
+ ],
+ 'rainlab.pages.access_snippets' => [
+ 'tab' => 'rainlab.pages::lang.page.tab',
+ 'order' => 200,
+ 'label' => 'rainlab.pages::lang.page.access_snippets'
+ ]
+ ];
+ }
+
+ public function registerNavigation()
+ {
+ return [
+ 'pages' => [
+ 'label' => 'rainlab.pages::lang.plugin.name',
+ 'url' => Backend::url('rainlab/pages'),
+ 'icon' => 'icon-files-o',
+ 'iconSvg' => 'plugins/rainlab/pages/assets/images/pages-icon.svg',
+ 'permissions' => ['rainlab.pages.*'],
+ 'order' => 200,
+
+ 'sideMenu' => [
+ 'pages' => [
+ 'label' => 'rainlab.pages::lang.page.menu_label',
+ 'icon' => 'icon-files-o',
+ 'url' => 'javascript:;',
+ 'attributes' => ['data-menu-item'=>'pages'],
+ 'permissions' => ['rainlab.pages.manage_pages']
+ ],
+ 'menus' => [
+ 'label' => 'rainlab.pages::lang.menu.menu_label',
+ 'icon' => 'icon-sitemap',
+ 'url' => 'javascript:;',
+ 'attributes' => ['data-menu-item'=>'menus'],
+ 'permissions' => ['rainlab.pages.manage_menus']
+ ],
+ 'content' => [
+ 'label' => 'rainlab.pages::lang.content.menu_label',
+ 'icon' => 'icon-file-text-o',
+ 'url' => 'javascript:;',
+ 'attributes' => ['data-menu-item'=>'content'],
+ 'permissions' => ['rainlab.pages.manage_content']
+ ],
+ 'snippets' => [
+ 'label' => 'rainlab.pages::lang.snippet.menu_label',
+ 'icon' => 'icon-newspaper-o',
+ 'url' => 'javascript:;',
+ 'attributes' => ['data-menu-item'=>'snippet'],
+ 'permissions' => ['rainlab.pages.access_snippets']
+ ]
+ ]
+ ]
+ ];
+ }
+
+ public function registerFormWidgets()
+ {
+ return [
+ FormWidgets\PagePicker::class => 'staticpagepicker',
+ FormWidgets\MenuPicker::class => 'staticmenupicker',
+ ];
+ }
+
+ public function boot()
+ {
+ Event::listen('cms.router.beforeRoute', function($url) {
+ return Controller::instance()->initCmsPage($url);
+ });
+
+ Event::listen('cms.page.beforeRenderPage', function($controller, $page) {
+ /*
+ * Before twig renders
+ */
+ $twig = $controller->getTwig();
+ $loader = $controller->getLoader();
+ Controller::instance()->injectPageTwig($page, $loader, $twig);
+
+ /*
+ * Get rendered content
+ */
+ $contents = Controller::instance()->getPageContents($page);
+ if (strlen($contents)) {
+ return $contents;
+ }
+ });
+
+ Event::listen('cms.page.initComponents', function($controller, $page) {
+ Controller::instance()->initPageComponents($controller, $page);
+ });
+
+ Event::listen('cms.block.render', function($blockName, $blockContents) {
+ $page = CmsController::getController()->getPage();
+
+ if (!isset($page->apiBag['staticPage'])) {
+ return;
+ }
+
+ $contents = Controller::instance()->getPlaceholderContents($page, $blockName, $blockContents);
+ if (strlen($contents)) {
+ return $contents;
+ }
+ });
+
+ Event::listen('pages.menuitem.listTypes', function() {
+ return [
+ 'static-page' => 'rainlab.pages::lang.menuitem.static_page',
+ 'all-static-pages' => 'rainlab.pages::lang.menuitem.all_static_pages'
+ ];
+ });
+
+ Event::listen('pages.menuitem.getTypeInfo', function($type) {
+ if ($type == 'url') {
+ return [];
+ }
+
+ if ($type == 'static-page'|| $type == 'all-static-pages') {
+ return StaticPage::getMenuTypeInfo($type);
+ }
+ });
+
+ Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) {
+ if ($type == 'static-page' || $type == 'all-static-pages') {
+ return StaticPage::resolveMenuItem($item, $url, $theme);
+ }
+ });
+
+ Event::listen('backend.form.extendFieldsBefore', function($formWidget) {
+ if ($formWidget->model instanceof \Cms\Classes\Partial) {
+ Snippet::extendPartialForm($formWidget);
+ }
+ });
+
+ Event::listen('cms.template.getTemplateToolbarSettingsButtons', function($extension, $dataHolder) {
+ if ($dataHolder->templateType === 'partial') {
+ Snippet::extendEditorPartialToolbar($dataHolder);
+ }
+ });
+
+ Event::listen('cms.template.save', function($controller, $template, $type) {
+ Plugin::clearCache();
+ });
+
+ Event::listen('cms.template.processSettingsBeforeSave', function($controller, $dataHolder) {
+ $dataHolder->settings = Snippet::processTemplateSettingsArray($dataHolder->settings);
+ });
+
+ Event::listen('cms.template.processSettingsAfterLoad', function($controller, $template, $context = null) {
+ Snippet::processTemplateSettings($template, $context);
+ });
+
+ Event::listen('cms.template.processTwigContent', function($template, $dataHolder) {
+ if ($template instanceof \Cms\Classes\Layout) {
+ $dataHolder->content = Controller::instance()->parseSyntaxFields($dataHolder->content);
+ }
+ });
+
+ Event::listen('backend.richeditor.listTypes', function () {
+ return [
+ 'static-page' => 'rainlab.pages::lang.menuitem.static_page',
+ ];
+ });
+
+ Event::listen('backend.richeditor.getTypeInfo', function ($type) {
+ if ($type === 'static-page') {
+ return StaticPage::getRichEditorTypeInfo($type);
+ }
+ });
+
+ Event::listen('system.console.theme.sync.getAvailableModelClasses', function () {
+ return [
+ Classes\Menu::class,
+ Classes\Page::class,
+ ];
+ });
+ }
+
+ /**
+ * Register new Twig variables
+ * @return array
+ */
+ public function registerMarkupTags()
+ {
+ return [
+ 'filters' => [
+ 'staticPage' => ['RainLab\Pages\Classes\Page', 'url']
+ ]
+ ];
+ }
+
+ public static function clearCache()
+ {
+ $theme = Theme::getEditTheme();
+
+ $router = new Router($theme);
+ $router->clearCache();
+
+ StaticPage::clearMenuCache($theme);
+ SnippetManager::clearCache($theme);
+ }
+}
diff --git a/plugins/rainlab/pages/README.md b/plugins/rainlab/pages/README.md
new file mode 100644
index 000000000..ccb976850
--- /dev/null
+++ b/plugins/rainlab/pages/README.md
@@ -0,0 +1,91 @@
+# Pages plugin
+
+This plugin allows end users to create and edit static pages and menus with a simple WYSIWYG user interface.
+
+## Managing static pages
+
+Static pages are managed on the Pages tab of the Static Pages plugin. Static pages have three required parameters - **Title**, **URL** and **Layout**. The URL is generated automatically when the Title is entered, but it could be changed manually. URLs must start with the forward slash character. The Layout drop-down allows to select a layout created with the CMS. Only layouts that include the `staticPage` component are displayed in the drop-down.
+
+ {.img-responsive .frame}
+
+Pages are hierarchical. The page hierarchy is used when a new page URL is generated, but as URLs can be changed manually, the hierarchy doesn't really affect the routing process. The only place where the page hierarchy matters is the generated Menus. The generated menus reflect the page hierarchy. You can manage the page hierarchy by dragging pages in the page tree. The page drag handle appears when you move the mouse cursor over page item in the tree.
+
+Optional properties of static pages are **Hidden** and **Hide in navigation**. The **Hidden** checkbox allows to hide a page from the front-end. Hidden pages are still visible for administrators who are logged into the back-end. The **Hide in navigation** checkbox allows to hide a page from generated menus and breadcrumbs.
+
+## Placeholders
+
+If a static layout contains [placeholders](https://octobercms.com/docs/cms/layouts#placeholders), the static page editor will show tabs for editing the placeholder contents. The plugin automatically detects text and HTML placeholders and displays a corresponding editor for them - the WYSIWYG editor for HTML placeholders and a text editor for text placeholders.
+
+## Snippets
+
+Snippets are elements that can be added by a Static Page, in the rich text editor. They allow to inject complex (and interactive) areas to pages. There are many possible applications and examples of using Snippets:
+
+* Google Maps snippet - outputs a map centered on specific coordinates with predefined zoom factor. That snippet would be great for static pages that explain directions.
+* Universal commenting system - allows visitors to post comments to any static page.
+* Third-party integrations - for example with Yelp or TripAdvisor for displaying extra information on a static page.
+
+Snippets are displayed in the sidebar list on the Static Pages and can be added into a rich editor with a mouse click. Snippets are configurable and have properties that users can manage with the Inspector.
+
+
+
+
+
+## Managing menus
+
+You can manage menus on the Menus tab of the Static Pages plugin. A website can contain multiple menus, for example the main menu, footer menu, sidebar menu, etc. A theme developer can include menus on a page layout with the `staticMenu` component.
+
+Menus have two required properties - the menu **Name** and menu **Code**. The menu name is displayed in the menu list in the back-end. The menu code is required for referring menus in the layout code, it's the API parameter.
+
+ {.img-responsive .frame}
+
+Menus can contain multiple **menu items**, and menu items can be nested. Each menu item has a number of properties. There are properties common for all menu item types, and some properties depend on the item type. The common menu item properties are **Title** and **Type**. The Title defines the menu item text. The Type is a drop-down list which displays all menu item types available in your OctoberCMS copy.
+
+ {.img-responsive .frame}
+
+#### Standard menu item types
+The available menu item types depend on the installed plugins, but there are three basic item types that are supported out of the box.
+
+###### Header {.subheader}
+Items of this type are used for displaying text and don't link to anything. The text could be used as a category heading for other menu items. This type will only show a title property.
+
+###### URL {.subheader}
+Items of this type are links to a specific fixed URL. That could be an URL of an or internal page. Items of this type don't have any other properties - just the title and URL.
+
+###### Static Page {.subheader}
+Items of this type refer to static pages. The static page should be selected in the **Reference** drop-down list described below.
+
+###### All Static Pages {.subheader}
+Items of this type expand to create links to all static pages defined in the theme. Nested pages are represented with nested menu items.
+
+#### Custom menu item types
+Other plugins can supply new menu item types. For example, the [Blog plugin](https://octobercms.com/plugin/rainlab-blog) by [RainLab](https://octobercms.com/author/RainLab) supplies two more types:
+
+###### Blog Category {.subheader}
+An item of this type represents a link to a specific blog category. The category should be selected in the **Reference** drop-down. This menu type also requires selecting a **CMS page** that outputs a blog category.
+
+###### All Blog Categories {.subheader}
+An item of this time expands into multiple items representing all blog existing categories. This menu type also requires selecting a **CMS page**.
+
+#### Menu item properties
+Depending on the selected menu item time you might need to provide other properties of the menu item. The available properties are described below.
+
+###### Reference {.subheader}
+A drop-down list of objects the menu item should refer to. The list content depends on the menu item type. For the **Static page** item type the list displays all static pages defined in the system. For the **Blog category** item type the list displays a list of blog categories.
+
+###### Allow nested items {.subheader}
+This checkbox is available only for menu item types that suppose nested objects. For example, static pages are hierarchical, and this property is available for the **Static page** item type. On the other hand, blog categories are not hierarchical, and the checkbox is hidden.
+
+###### Replace this item with its generated children {.subheader}
+A checkbox determining whether the menu item should be replaced with generated menu items. This property is available only for menu item types that suppose automatic item generating, for example for the **Static page** menu item type. The **Blog category** menu item type doesn't have this property because blog categories cannot be nested and menu items of this type always point to a specific blog category. This property is very handy when you want to include generated menu items to the root of the menu. For example, you can create the **All blog categories** menu item and enable the replacing. As a result you will get a menu that lists all blog categories on the first level of the menu. If you didn't enable the replacing, there would be a root menu item, with blog categories listed under it.
+
+###### CMS Page {.subheader}
+This drop-down is available for menu item types that require a special CMS page to refer to. For example, the **Blog category** menu item type requires a CMS page that hosts the `blogPosts` component. The CMS Page drop-down for this item type will only display pages that include this component.
+
+###### Code {.subheader}
+The Code field allows to assign the API code that you can use to set the active menu item explicitly in the page's `onInit()` handler described in the documentation.
+
+## See also
+
+Read the [Getting started with Static Pages](https://octobercms.com/blog/post/getting-started-static-pages) tutorial in the Blog.
+
+Read the [documentation](/docs/documentation.md).
diff --git a/plugins/rainlab/pages/assets/css/pages.css b/plugins/rainlab/pages/assets/css/pages.css
new file mode 100644
index 000000000..20223d75f
--- /dev/null
+++ b/plugins/rainlab/pages/assets/css/pages.css
@@ -0,0 +1,133 @@
+.control-treeview ol > li.dragged div,
+.control-treeview ol > li > div:hover {
+ background-color: #4ea5e0 !important;
+}
+.control-treeview ol >li > div:active {
+ background-color: #3498db !important;
+}
+.control-filelist.menu-list li > a {
+ position: relative;
+}
+.control-filelist.menu-list li > a:before {
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ left: 17px;
+ top: 18px;
+ content: ' ';
+ background-image: url(../images/menu-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 36px auto;
+}
+.control-filelist.menu-list li > a:hover:before {
+ background-position: 0 -60px;
+}
+.control-filelist.content li > a {
+ position: relative;
+}
+.control-filelist.content li > a:before {
+ position: absolute;
+ width: 18px;
+ height: 22px;
+ left: 18px;
+ top: 10px;
+ content: ' ';
+ background-image: url(../images/content-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 34px auto;
+}
+.control-filelist.content li > a:hover:before {
+ background-position: 0 -27px;
+}
+.control-filelist.content li.group ul li > a:before {
+ left: 34px;
+}
+.control-filelist.snippet-list li > a {
+ position: relative;
+ color: #808c8d;
+}
+.control-filelist.snippet-list li > a:before {
+ position: absolute;
+ width: 17px;
+ height: 19px;
+ content: ' ';
+ background-image: url(../images/snippet-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 34px auto;
+ left: 18px;
+ top: 13px;
+}
+.control-filelist.snippet-list li > a:hover:before {
+ background-position: 0 -21px;
+}
+.control-filelist.snippet-list li.group ul li > a:before {
+ left: 34px;
+}
+@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
+ .control-filelist.menu-list li > a:before {
+ background-position: 0px -11px;
+ background-size: 18px auto;
+ }
+ .control-filelist.menu-list li > a:hover:before {
+ background-position: 0px -40px;
+ }
+ .control-filelist.content li a:before {
+ background-position: 0px -27px;
+ background-size: 17px auto;
+ }
+ .control-filelist.content li a:hover:before {
+ background-position: 0px -52px;
+ }
+ .control-filelist.snippet-list li a:before {
+ background-position: 0px -21px;
+ background-size: 17px auto;
+ }
+ .control-filelist.snippet-list li a:hover:before {
+ background-position: 0px -41px;
+ }
+}
+.fancy-layout .pagesTextEditor {
+ border-left: 1px solid #bdc3c7 !important;
+}
+.control-richeditor [data-snippet]:before {
+ content: attr(data-name);
+}
+.control-richeditor [data-snippet]:after {
+ position: absolute;
+ width: 17px;
+ height: 19px;
+ left: 18px;
+ top: 17px;
+ content: ' ';
+ background-image: url(../images/snippet-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 34px auto;
+ left: 11px;
+ top: 12px;
+}
+.control-richeditor [data-snippet].loading:after {
+ background-image: url(../../../../../modules/system/assets/ui/images/loader-transparent.svg);
+ background-size: 15px 15px;
+ background-position: 50% 50%;
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ top: 13px;
+ content: ' ';
+ -webkit-animation: spin 1s linear infinite;
+ animation: spin 1s linear infinite;
+}
+@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
+ .control-richeditor [data-snippet]:after {
+ background-position: 0px -21px;
+ background-size: 17px auto;
+ }
+}
+.form-group.secondary-tab {
+ padding: 10px 20px;
+ background: #fff;
+}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/assets/images/content-icons.png b/plugins/rainlab/pages/assets/images/content-icons.png
new file mode 100644
index 000000000..893dc71ae
Binary files /dev/null and b/plugins/rainlab/pages/assets/images/content-icons.png differ
diff --git a/plugins/rainlab/pages/assets/images/menu-icons.png b/plugins/rainlab/pages/assets/images/menu-icons.png
new file mode 100644
index 000000000..f3641e9f2
Binary files /dev/null and b/plugins/rainlab/pages/assets/images/menu-icons.png differ
diff --git a/plugins/rainlab/pages/assets/images/pages-icon.svg b/plugins/rainlab/pages/assets/images/pages-icon.svg
new file mode 100644
index 000000000..f17deac95
--- /dev/null
+++ b/plugins/rainlab/pages/assets/images/pages-icon.svg
@@ -0,0 +1,31 @@
+
+
+
+ pages-icon
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/assets/images/snippet-icons.png b/plugins/rainlab/pages/assets/images/snippet-icons.png
new file mode 100644
index 000000000..3d0b66f89
Binary files /dev/null and b/plugins/rainlab/pages/assets/images/snippet-icons.png differ
diff --git a/plugins/rainlab/pages/assets/js/pages-page.js b/plugins/rainlab/pages/assets/js/pages-page.js
new file mode 100644
index 000000000..e969e2d0f
--- /dev/null
+++ b/plugins/rainlab/pages/assets/js/pages-page.js
@@ -0,0 +1,644 @@
+/*
+ * Handles the Pages main page.
+ */
++function ($) { "use strict";
+ var Base = $.oc.foundation.base,
+ BaseProto = Base.prototype
+
+ var PagesPage = function () {
+ Base.call(this)
+
+ this.init()
+ }
+
+ PagesPage.prototype = Object.create(BaseProto)
+ PagesPage.prototype.constructor = PagesPage
+
+ PagesPage.prototype.init = function() {
+ this.$masterTabs = $('#pages-master-tabs')
+ this.$sidePanel = $('#pages-side-panel')
+ this.$pageTree = $('[data-control=treeview]', this.$sidePanel)
+ this.masterTabsObj = this.$masterTabs.data('oc.tab')
+ this.snippetManager = new $.oc.pages.snippetManager(this.$masterTabs)
+
+ this.registerHandlers()
+ }
+
+ PagesPage.prototype.registerHandlers = function() {
+ // Item is clicked in the sidebar
+ $(document).on('open.oc.treeview', 'form.layout[data-content-id=pages]', this.proxy(this.onSidebarItemClick))
+
+ $(document).on('open.oc.list', this.$sidePanel, this.proxy(this.onSidebarItemClick))
+
+ // A tab is shown / switched
+ this.$masterTabs.on('shown.bs.tab', this.proxy(this.onTabShown))
+
+ // All master tabs are closed
+ this.$masterTabs.on('afterAllClosed.oc.tab', this.proxy(this.onAllTabsClosed))
+
+ // A master tab is closed
+ this.$masterTabs.on('closed.oc.tab', this.proxy(this.onTabClosed))
+
+ // AJAX errors in the master tabs area
+ $(document).on('ajaxError', '#pages-master-tabs form', this.proxy(this.onAjaxError))
+
+ // AJAX success in the master tabs area
+ $(document).on('ajaxSuccess', '#pages-master-tabs form', this.proxy(this.onAjaxSuccess))
+
+ // Before save a content block
+ $(document).on('oc.beforeRequest', '#pages-master-tabs form[data-object-type=content]', this.proxy(this.onBeforeSaveContent))
+
+ // Layout changed
+ $(document).on('change', '#pages-master-tabs form[data-object-type=page] select[name="viewBag[layout]"]', this.proxy(this.onLayoutChanged))
+
+ // Create object button click
+ $(document).on(
+ 'click',
+ '#pages-side-panel form [data-control=create-object], #pages-side-panel form [data-control=create-template]',
+ this.proxy(this.onCreateObject)
+ )
+
+ // Submenu item is clicked in the sidebar
+ $(document).on('submenu.oc.treeview', 'form.layout[data-content-id=pages]', this.proxy(this.onSidebarSubmenuItemClick))
+
+ // The Delete Object button click
+ $(document).on('click', '#pages-side-panel form button[data-control=delete-object], #pages-side-panel form button[data-control=delete-template]',
+ this.proxy(this.onDeleteObject))
+
+ // A new tab is added to the editor
+ this.$masterTabs.on('initTab.oc.tab', this.proxy(this.onInitTab))
+
+ // Handle the menu saving
+ $(document).on('oc.beforeRequest', '#pages-master-tabs form[data-object-type=menu]', this.proxy(this.onSaveMenu))
+ }
+
+ /*
+ * Displays the concurrency resolution form.
+ */
+ PagesPage.prototype.handleMtimeMismatch = function (form) {
+ var $form = $(form)
+
+ $form.popup({ handler: 'onOpenConcurrencyResolveForm' })
+
+ var popup = $form.data('oc.popup'),
+ self = this
+
+ $(popup.$container).on('click', 'button[data-action=reload]', function(){
+ popup.hide()
+ self.reloadForm($form)
+ })
+
+ $(popup.$container).on('click', 'button[data-action=save]', function(){
+ popup.hide()
+
+ $('input[name=objectForceSave]', $form).val(1)
+ $('a[data-request=onSave]', $form).trigger('click')
+ $('input[name=objectForceSave]', $form).val(0)
+ })
+ }
+
+ /*
+ * Reloads the Editor form.
+ */
+ PagesPage.prototype.reloadForm = function($form) {
+ var data = {
+ type: $('[name=objectType]', $form).val(),
+ theme: $('[name=theme]', $form).val(),
+ path: $('[name=objectPath]', $form).val(),
+ },
+ tabId = data.type + '-' + data.theme + '-' + data.path,
+ tab = this.masterTabsObj.findByIdentifier(tabId),
+ self = this
+
+ /*
+ * Update tab
+ */
+
+ $.oc.stripeLoadIndicator.show()
+ $form.request('onOpen', {
+ data: data
+ }).done(function(data) {
+ $.oc.stripeLoadIndicator.hide()
+ self.$masterTabs.ocTab('updateTab', tab, data.tabTitle, data.tab)
+ self.$masterTabs.ocTab('unmodifyTab', tab)
+ self.updateModifiedCounter()
+ }).always(function(){
+ $.oc.stripeLoadIndicator.hide()
+ })
+ }
+
+ /*
+ * Updates the sidebar counter
+ */
+ PagesPage.prototype.updateModifiedCounter = function() {
+ var counters = {
+ page: {menu: 'pages', count: 0},
+ menu: {menu: 'menus', count: 0},
+ content: {menu: 'content', count: 0},
+ }
+
+ $('> div.tab-content > div.tab-pane[data-modified]', this.$masterTabs).each(function(){
+ var inputType = $('> form > input[name=objectType]', this).val()
+ counters[inputType].count++
+ })
+
+ $.each(counters, function(type, data){
+ $.oc.sideNav.setCounter('pages/' + data.menu, data.count);
+ })
+ }
+
+ /*
+ * Triggered when a tab is displayed. Updated the current selection in the sidebar and sets focus on an editor.
+ */
+ PagesPage.prototype.onTabShown = function(e) {
+ var $tabControl = $(e.target).closest('[data-control=tab]')
+
+ if ($tabControl.attr('id') == 'pages-master-tabs') {
+ var dataId = $(e.target).closest('li').attr('data-tab-id'),
+ title = $(e.target).attr('title')
+
+ if (title)
+ this.setPageTitle(title)
+
+ this.$pageTree.treeView('markActive', dataId)
+ $('[data-control=filelist]', this.$sidePanel).fileList('markActive', dataId)
+ $(window).trigger('resize')
+ } else if ($tabControl.hasClass('secondary')) {
+ // TODO: Focus the code or rich editor here
+ }
+ }
+
+ /*
+ * Triggered when all master tabs are closed.
+ */
+ PagesPage.prototype.onAllTabsClosed = function() {
+ this.$pageTree.treeView('markActive', null)
+ $('[data-control=filelist]', this.$sidePanel).fileList('markActive', null)
+ this.setPageTitle('')
+ }
+
+ /*
+ * Triggered when a master tab is closed.
+ */
+ PagesPage.prototype.onTabClosed = function() {
+ this.updateModifiedCounter()
+ }
+
+ /*
+ * Handles AJAX errors in the master tab forms. Processes the mtime mismatch condition (concurrency).
+ */
+ PagesPage.prototype.onAjaxError = function(event, context, message, data, jqXHR) {
+ if (context.handler != 'onSave')
+ return
+
+ if (jqXHR.responseText == 'mtime-mismatch') {
+ event.preventDefault()
+ this.handleMtimeMismatch(event.target)
+ }
+ }
+
+ /*
+ * Handles successful AJAX request in the master tab forms. Updates the UI elements and resets the mtime value.
+ */
+ PagesPage.prototype.onAjaxSuccess = function(event, context, data) {
+ var $form = $(event.currentTarget),
+ $tabPane = $form.closest('.tab-pane')
+
+ // Update the visibilities of the commit & reset buttons
+ $('[data-control=commit-button]', $form).toggleClass('hide', !data.canCommit)
+ $('[data-control=reset-button]', $form).toggleClass('hide', !data.canReset)
+
+ if (data.objectPath !== undefined) {
+ $('input[name=objectPath]', $form).val(data.objectPath)
+ $('input[name=objectMtime]', $form).val(data.objectMtime)
+ $('[data-control=delete-button]', $form).removeClass('hide')
+ $('[data-control=preview-button]', $form).removeClass('hide')
+
+ if (data.pageUrl !== undefined)
+ $('[data-control=preview-button]', $form).attr('href', data.pageUrl)
+ }
+
+ if (data.tabTitle !== undefined) {
+ this.$masterTabs.ocTab('updateTitle', $tabPane, data.tabTitle)
+ this.setPageTitle(data.tabTitle)
+ }
+
+ var tabId = $('input[name=objectType]', $form).val() + '-'
+ + $('input[name=theme]', $form).val() + '-'
+ + $('input[name=objectPath]', $form).val();
+
+ this.$masterTabs.ocTab('updateIdentifier', $tabPane, tabId)
+ this.$pageTree.treeView('markActive', tabId)
+ $('[data-control=filelist]', this.$sidePanel).fileList('markActive', tabId)
+
+ var objectType = $('input[name=objectType]', $form).val()
+ if (objectType.length > 0 &&
+ (context.handler == 'onSave' || context.handler == 'onCommit' || context.handler == 'onReset')
+ )
+ this.updateObjectList(objectType)
+
+ if (context.handler == 'onSave' && (!data['X_OCTOBER_ERROR_FIELDS'] && !data['X_OCTOBER_ERROR_MESSAGE']))
+ $form.trigger('unchange.oc.changeMonitor')
+
+ // Reload the form if the server has requested it
+ if (data.forceReload) {
+ this.reloadForm($form)
+ }
+ }
+
+ PagesPage.prototype.onBeforeSaveContent = function(e, data) {
+ var form = e.currentTarget,
+ $tabPane = $(form).closest('.tab-pane')
+
+ this.updateContentEditorMode($tabPane, false)
+ }
+
+ PagesPage.prototype.onDeletePageSingle = function(el) {
+ var $el = $(el);
+
+ $el.request('onDelete', {
+ success: function(data) {
+ $.oc.pagesPage.closeTabs(data, 'page');
+ $.oc.pagesPage.updateObjectList('page');
+ $(this).trigger('close.oc.tab', [{force: true}]);
+ }
+ });
+ }
+
+ /*
+ * Updates the browser title when an object is saved.
+ */
+ PagesPage.prototype.setPageTitle = function(title) {
+ $.oc.layout.setPageTitle(title.length ? (title + ' | ') : title)
+ }
+
+ /*
+ * Updates the sidebar object list.
+ */
+ PagesPage.prototype.updateObjectList = function(objectType) {
+ var $form = $('form[data-object-type='+objectType+']', this.$sidePanel),
+ objectList = objectType + 'List',
+ self = this
+
+ $.oc.stripeLoadIndicator.show()
+ $form.request(objectList + '::onUpdate', {
+ complete: function(data) {
+ $('button[data-control=delete-object], button[data-control=delete-template]', $form).trigger('oc.triggerOn.update')
+ }
+ }).always(function(){
+ $.oc.stripeLoadIndicator.hide()
+ });
+ }
+
+ /*
+ * Closes deleted page tabs in the editor area.
+ */
+ PagesPage.prototype.closeTabs = function(data, type) {
+ var self = this
+
+ $.each(data.deletedObjects, function(){
+ var tabId = type + '-' + data.theme + '-' + this,
+ tab = self.masterTabsObj.findByIdentifier(tabId)
+
+ $(tab).trigger('close.oc.tab', [{force: true}])
+ })
+ }
+
+ /*
+ * Triggered when an item is clicked in the sidebar. Opens the item in the editor.
+ * If the item is already opened, activate its tab in the editor.
+ */
+ PagesPage.prototype.onSidebarItemClick = function(e) {
+ var self = this,
+ $item = $(e.relatedTarget),
+ $form = $item.closest('form'),
+ theme = $('input[name=theme]', $form).val(),
+ data = {
+ type: $form.data('object-type'),
+ theme: theme,
+ path: $item.data('item-path')
+ },
+ tabId = data.type + '-' + data.theme + '-' + data.path
+
+ if ($item.data('type') == 'snippet') {
+ this.snippetManager.onSidebarSnippetClick($item)
+
+ return
+ }
+
+ /*
+ * Find if the tab is already opened
+ */
+
+ if (this.masterTabsObj.goTo(tabId))
+ return false
+
+ /*
+ * Open a new tab
+ */
+
+ $.oc.stripeLoadIndicator.show()
+ $form.request('onOpen', {
+ data: data
+ }).done(function(data) {
+ self.$masterTabs.ocTab('addTab', data.tabTitle, data.tab, tabId, $form.data('type-icon'))
+ }).always(function() {
+ $.oc.stripeLoadIndicator.hide()
+ })
+
+ return false
+ }
+
+ /*
+ * Triggered when the Add button is clicked on the sidebar
+ */
+ PagesPage.prototype.onCreateObject = function(e) {
+ var self = this,
+ $button = $(e.target),
+ $form = $button.closest('form'),
+ parent = $button.data('parent') !== undefined ? $button.data('parent') : null,
+ type = $form.data('object-type') ? $form.data('object-type') : $form.data('template-type'),
+ tabId = type + Math.random()
+
+ $.oc.stripeLoadIndicator.show()
+ $form.request('onCreateObject', {
+ data: {
+ type: type,
+ parent: parent
+ }
+ }).done(function(data){
+ self.$masterTabs.ocTab('addTab', data.tabTitle, data.tab, tabId, $form.data('type-icon') + ' new-template')
+ $('#layout-side-panel').trigger('close.oc.sidePanel')
+ self.setPageTitle(data.tabTitle)
+ }).always(function(){
+ $.oc.stripeLoadIndicator.hide()
+ })
+
+ e.stopPropagation()
+
+ return false
+ }
+
+ /*
+ * Triggered when an item is clicked in the sidebar submenu
+ */
+ PagesPage.prototype.onSidebarSubmenuItemClick = function(e) {
+ if ($(e.clickEvent.target).data('control') == 'create-object')
+ this.onCreateObject(e.clickEvent)
+
+ return false
+ }
+
+ /*
+ * Triggered when the Delete button is clicked on the sidebar
+ */
+ PagesPage.prototype.onDeleteObject = function(e) {
+ var $el = $(e.target),
+ $form = $el.closest('form'),
+ objectType = $form.data('object-type'),
+ self = this
+
+ if (!confirm($el.data('confirmation')))
+ return
+
+ $form.request('onDeleteObjects', {
+ data: {
+ type: objectType
+ },
+ success: function(data) {
+ $.each(data.deleted, function(index, path){
+ var tabId = objectType + '-' + data.theme + '-' + path,
+ tab = self.masterTabsObj.findByIdentifier(tabId)
+
+ self.$masterTabs.ocTab('closeTab', tab, true)
+ })
+
+ if (data.error !== undefined && $.type(data.error) === 'string' && data.error.length)
+ $.oc.flashMsg({text: data.error, 'class': 'error'})
+ },
+ complete: function() {
+ self.updateObjectList(objectType)
+ }
+ })
+
+ return false
+ }
+
+ /*
+ * Triggered when a static page layout changes
+ */
+ PagesPage.prototype.onLayoutChanged = function(e) {
+ var
+ self = this,
+ $el = $(e.target),
+ $form = $el.closest('form'),
+ $pane = $form.closest('.tab-pane'),
+ data = {
+ type: $('[name=objectType]', $form).val(),
+ theme: $('[name=theme]', $form).val(),
+ path: $('[name=objectPath]', $form).val()
+ },
+ tab = $pane.data('tab')
+
+ // $form.trigger('unchange.oc.changeMonitor')
+ $form.changeMonitor('dispose')
+
+ $.oc.stripeLoadIndicator.show()
+
+ $form
+ .request('onUpdatePageLayout', {
+ data: data
+ })
+ .done(function(data){
+ self.$masterTabs.ocTab('updateTab', tab, data.tabTitle, data.tab)
+ })
+ .always(function(){
+ $.oc.stripeLoadIndicator.hide()
+ $('form:first', $pane).changeMonitor().trigger('change')
+ })
+ }
+
+ /*
+ * Triggered when a new tab is added to the Editor
+ */
+ PagesPage.prototype.onInitTab = function(e, data) {
+ if ($(e.target).attr('id') != 'pages-master-tabs')
+ return
+
+ var $collapseIcon = $(' '),
+ $panel = $('.form-tabless-fields', data.pane),
+ $secondaryPanel = $('.control-tabs.secondary-tabs', data.pane),
+ $primaryPanel = $('.control-tabs.primary-tabs', data.pane),
+ hasSecondaryTabs = $secondaryPanel.length > 0
+
+ $secondaryPanel.addClass('secondary-content-tabs')
+
+ $panel.append($collapseIcon)
+
+ if (!hasSecondaryTabs) {
+ $('.primary-tabs').parent().removeClass('min-size')
+ }
+
+ $collapseIcon.click(function(){
+ $panel.toggleClass('collapsed')
+
+ if (typeof(localStorage) !== 'undefined')
+ localStorage.ocPagesTablessCollapsed = $panel.hasClass('collapsed') ? 1 : 0
+
+ window.setTimeout(function(){
+ $(window).trigger('oc.updateUi')
+ }, 500)
+
+ return false
+ })
+
+ var $primaryCollapseIcon = $(' ')
+
+ if ($primaryPanel.length > 0) {
+ $secondaryPanel.append($primaryCollapseIcon)
+
+ $primaryCollapseIcon.click(function(){
+ $primaryPanel.toggleClass('collapsed')
+ $secondaryPanel.toggleClass('primary-collapsed')
+ $(window).trigger('oc.updateUi')
+ if (typeof(localStorage) !== 'undefined')
+ localStorage.ocPagesPrimaryCollapsed = $primaryPanel.hasClass('collapsed') ? 1 : 0
+ return false
+ })
+ } else {
+ $secondaryPanel.addClass('primary-collapsed')
+ }
+
+ if (typeof(localStorage) !== 'undefined') {
+ if (!$('a', data.tab).hasClass('new-template') && localStorage.ocPagesTablessCollapsed == 1)
+ $panel.addClass('collapsed')
+
+ if (localStorage.ocPagesPrimaryCollapsed == 1 && hasSecondaryTabs) {
+ $primaryPanel.addClass('collapsed')
+ $secondaryPanel.addClass('primary-collapsed')
+ }
+ }
+
+ var $form = $('form', data.pane),
+ self = this,
+ $panel = $('.form-tabless-fields', data.pane)
+
+ this.updateContentEditorMode(data.pane, true)
+
+ $form.on('changed.oc.changeMonitor', function() {
+ $panel.trigger('modified.oc.tab')
+ $panel.find('[data-control=commit-button]').addClass('hide');
+ $panel.find('[data-control=reset-button]').addClass('hide');
+ self.updateModifiedCounter()
+ })
+
+ $form.on('unchanged.oc.changeMonitor', function() {
+ $panel.trigger('unmodified.oc.tab')
+ self.updateModifiedCounter()
+ })
+ }
+
+ /*
+ * Triggered before a menu is saved
+ */
+ PagesPage.prototype.onSaveMenu = function(e, data) {
+ var form = e.currentTarget,
+ items = [],
+ $items = $('div[data-control=treeview] > ol > li', form)
+
+ var iterator = function(items) {
+ var result = []
+
+ $.each(items, function() {
+ var item = $(this).data('menu-item')
+
+ var $subitems = $('> ol >li', this)
+ if ($subitems.length)
+ item['items'] = iterator($subitems)
+
+ result.push(item)
+ })
+
+ return result
+ }
+
+ data.options.data['itemData'] = JSON.stringify(iterator($items))
+ }
+
+ /*
+ * Updates the content editor to correspond to the content file extension
+ */
+ PagesPage.prototype.updateContentEditorMode = function(pane, initialization) {
+ if ($('[data-toolbar-type]', pane).data('toolbar-type') !== 'content')
+ return
+
+ var extension = this.getContentExtension(pane),
+ mode = 'html',
+ editor = $('[data-control=codeeditor]', pane)
+
+ if (extension == 'html')
+ extension = 'htm'
+
+ if (initialization)
+ $(pane).data('prev-extension', extension)
+
+ if (extension == 'htm') {
+ $('[data-field-name=markup]', pane).hide()
+ $('[data-field-name=markup_html]', pane).show()
+
+ if (!initialization && $(pane).data('prev-extension') != 'htm') {
+ var val = editor.codeEditor('getContent')
+ $('div[data-control=richeditor]', pane).richEditor('setContent', val)
+ }
+ }
+ else {
+ $('[data-field-name=markup]', pane).show()
+ $('[data-field-name=markup_html]', pane).hide()
+
+ if (!initialization && $(pane).data('prev-extension') == 'htm') {
+ var val = $('div[data-control=richeditor]', pane).richEditor('getContent')
+ editor.codeEditor('setContent', val)
+ }
+
+ var modes = $.oc.codeEditorExtensionModes
+
+ if (modes[extension] !== undefined)
+ mode = modes[extension];
+
+ var setEditorMode = function() {
+ window.setTimeout(function(){
+ editor.codeEditor('getEditorObject')
+ .getSession()
+ .setMode({ path: 'ace/mode/'+mode })
+ }, 200)
+ }
+
+ if (initialization)
+ editor.on('oc.codeEditorReady', setEditorMode)
+ else
+ setEditorMode()
+ }
+
+ if (!initialization)
+ $(pane).data('prev-extension', extension)
+ }
+
+ /*
+ * Returns the content file extension
+ */
+ PagesPage.prototype.getContentExtension = function(form) {
+ var $input = $('input[name=fileName]', form),
+ fileName = $input.length ? $input.val() : '',
+ parts = fileName.split('.')
+
+ if (parts.length >= 2)
+ return parts.pop().toLowerCase()
+
+ return 'htm'
+ }
+
+ $(document).ready(function(){
+ $.oc.pagesPage = new PagesPage()
+ })
+
+}(window.jQuery);
diff --git a/plugins/rainlab/pages/assets/js/pages-snippets.js b/plugins/rainlab/pages/assets/js/pages-snippets.js
new file mode 100644
index 000000000..16fd3be8c
--- /dev/null
+++ b/plugins/rainlab/pages/assets/js/pages-snippets.js
@@ -0,0 +1,213 @@
+/*
+ * Handles snippet operations on the Pages main page
+ */
++function ($) { "use strict";
+ if ($.oc.pages === undefined)
+ $.oc.pages = {}
+
+ var SnippetManager = function ($masterTabs) {
+ this.$masterTabs = $masterTabs
+
+ var self = this
+
+ $(document).on('hidden.oc.inspector', '.field-richeditor [data-snippet]', function() {
+ self.syncEditorCode(this)
+ })
+
+ $(document).on('init.oc.richeditor', '.field-richeditor textarea', function(ev, richeditor) {
+ self.initSnippets(richeditor.getElement())
+ })
+
+ $(document).on('setContent.oc.richeditor', '.field-richeditor textarea', function(ev, richeditor) {
+ if (!richeditor.isCodeViewActive()) {
+ self.initSnippets(richeditor.getElement())
+ }
+ })
+
+ $(document).on('syncContent.oc.richeditor', '.field-richeditor textarea', function(ev, richeditor, container) {
+ self.syncPageMarkup(ev, container)
+ })
+
+ $(document).on('figureKeydown.oc.richeditor', '.field-richeditor textarea', function(ev, originalEv, richeditor) {
+ self.editorKeyDown(ev, originalEv, richeditor)
+ })
+
+ $(document).on('click', '[data-snippet]', function() {
+ if ($(this).hasClass('inspector-open')) {
+ return
+ }
+
+ $.oc.inspector.manager.createInspector(this)
+ return false
+ })
+ }
+
+ SnippetManager.prototype.onSidebarSnippetClick = function($sidebarItem) {
+ var $pageForm = $('div.tab-content > .tab-pane.active form[data-object-type=page]', this.$masterTabs)
+
+ if (!$pageForm.length) {
+ $.oc.alert('Snippets can only be added to Pages. Please open or create a Page first.')
+ return
+ }
+
+ var $activeEditorTab = $('.control-tabs.secondary-tabs .tab-pane.active', $pageForm),
+ $textarea = $activeEditorTab.find('[data-control="richeditor"] textarea'),
+ $richeditorNode = $textarea.closest('[data-control="richeditor"]'),
+ $snippetNode = $(' '),
+ componentClass = $sidebarItem.attr('data-component-class'),
+ snippetCode = $sidebarItem.data('snippet')
+
+ if (!$textarea.length) {
+ $.oc.alert('Snippets can only be added to page Content or HTML placeholders.')
+ return
+ }
+
+ if (componentClass) {
+ $snippetNode.attr({
+ 'data-component': componentClass,
+ 'data-inspector-class': componentClass
+ })
+
+ // If a component-based snippet was added, make sure that
+ // its code is unique, as it will be used as a component
+ // alias.
+
+ snippetCode = this.generateUniqueComponentSnippetCode(componentClass, snippetCode, $pageForm)
+ }
+
+ $snippetNode.attr({
+ 'data-snippet': snippetCode,
+ 'data-name': $sidebarItem.data('snippet-name'),
+ 'tabindex': '0',
+ 'draggable': 'true',
+ 'data-ui-block': 'true'
+ })
+
+ $snippetNode.addClass('fr-draggable')
+
+ $richeditorNode.richEditor('insertUiBlock', $snippetNode)
+ }
+
+ SnippetManager.prototype.generateUniqueComponentSnippetCode = function(componentClass, originalCode, $pageForm) {
+ var updatedCode = originalCode,
+ counter = 1,
+ snippetFound = false
+
+ do {
+ snippetFound = false
+
+ $('[data-control="richeditor"] textarea', $pageForm).each(function(){
+ var $textarea = $(this),
+ $codeDom = $('' + $textarea.val() + '
')
+
+ if ($codeDom.find('[data-snippet="'+updatedCode+'"][data-component]').length > 0) {
+ snippetFound = true
+ updatedCode = originalCode + counter
+ counter++
+
+ return false
+ }
+ })
+
+ } while (snippetFound)
+
+ return updatedCode
+ }
+
+ SnippetManager.prototype.syncEditorCode = function(inspectable) {
+ // Race condition
+ setTimeout(function() {
+ var $richeditor = $(inspectable).closest('[data-control=richeditor]')
+ $richeditor.richEditor('syncContent')
+ inspectable.focus()
+ }, 0)
+ }
+
+ SnippetManager.prototype.initSnippets = function($editor) {
+ var snippetCodes = []
+
+ $('.fr-view [data-snippet]', $editor).each(function(){
+ var $snippet = $(this),
+ snippetCode = $snippet.attr('data-snippet'),
+ componentClass = $snippet.attr('data-component')
+
+ if (componentClass)
+ snippetCode += '|' + componentClass
+
+ snippetCodes.push(snippetCode)
+
+ $snippet
+ .addClass('loading')
+ .addClass('fr-draggable')
+ .attr({
+ 'data-inspector-css-class': 'hero',
+ 'data-name': 'Loading...',
+ 'data-ui-block': 'true',
+ 'draggable': 'true',
+ 'tabindex': '0'
+ })
+ .html(' ')
+
+ if (componentClass) {
+ $snippet.attr('data-inspector-class', componentClass)
+ }
+
+ this.contentEditable = false
+ })
+
+ if (snippetCodes.length > 0) {
+ var request = $editor.request('onGetSnippetNames', {
+ data: {
+ codes: snippetCodes
+ }
+ }).done(function(data) {
+ if (data.names !== undefined) {
+ $.each(data.names, function(code){
+ $('[data-snippet="'+code+'"]', $editor)
+ .attr('data-name', this)
+ .removeClass('loading')
+ })
+ }
+ })
+ }
+ }
+
+ SnippetManager.prototype.syncPageMarkup = function(ev, container) {
+ var $domTree = $(''+container.html+'
')
+
+ $('[data-snippet]', $domTree).each(function(){
+ var $snippet = $(this)
+
+ $snippet.removeAttr('contenteditable data-name tabindex data-inspector-css-class data-inspector-class data-property-inspectorclassname data-property-inspectorproperty data-ui-block draggable')
+ $snippet.removeClass('fr-draggable fr-dragging')
+
+ if (!$snippet.attr('class')) {
+ $snippet.removeAttr('class')
+ }
+ })
+
+ container.html = $domTree.html()
+ }
+
+ SnippetManager.prototype.editorKeyDown = function(ev, originalEv, richeditor) {
+ if (richeditor.getTextarea() === undefined)
+ return
+
+ if (originalEv.target && $(originalEv.target).attr('data-snippet') !== undefined) {
+ this.snippetKeyDown(originalEv, originalEv.target)
+ }
+ }
+
+ SnippetManager.prototype.snippetKeyDown = function(ev, snippet) {
+ if (ev.which == 32) {
+ switch (ev.which) {
+ case 32:
+ // Space key
+ $.oc.inspector.manager.createInspector(snippet)
+ break
+ }
+ }
+ }
+
+ $.oc.pages.snippetManager = SnippetManager
+}(window.jQuery);
diff --git a/plugins/rainlab/pages/assets/less/pages.less b/plugins/rainlab/pages/assets/less/pages.less
new file mode 100644
index 000000000..692219a5e
--- /dev/null
+++ b/plugins/rainlab/pages/assets/less/pages.less
@@ -0,0 +1,204 @@
+@import "../../../../../modules/backend/assets/less/core/boot.less";
+
+.control-treeview {
+ ol > li.dragged div,
+ ol >li > div:hover {
+ background-color: #4ea5e0 !important
+ }
+
+ ol >li > div:active {
+ background-color: #3498db !important;
+ }
+}
+
+.control-filelist.menu-list {
+ li {
+ > a {
+ position: relative;
+
+ &:before {
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ left: 17px;
+ top: 18px;
+
+ content: ' ';
+
+ background-image: url(../images/menu-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 36px auto;
+ }
+
+ &:hover {
+ &:before {
+ background-position: 0 -60px;
+ }
+ }
+ }
+ }
+}
+
+.control-filelist.content {
+ li > a {
+ position: relative;
+
+ &:before {
+ position: absolute;
+ width: 18px;
+ height: 22px;
+ left: 18px;
+ top: 10px;
+
+ content: ' ';
+
+ background-image: url(../images/content-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 34px auto;
+ }
+
+ &:hover {
+ &:before {
+ background-position: 0 -27px;
+ }
+ }
+ }
+
+ li.group ul li > a:before {
+ left: 34px;
+ }
+}
+
+.page-snippet-icon() {
+ position: absolute;
+ width: 17px;
+ height: 19px;
+ left: 18px;
+ top: 13px;
+
+ content: ' ';
+
+ background-image: url(../images/snippet-icons.png);
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ background-size: 34px auto;
+}
+
+.control-filelist.snippet-list {
+ li > a {
+ position: relative;
+ color: #808c8d;
+
+ &:before {
+ .page-snippet-icon();
+
+ left: 18px;
+ top: 17px;
+ }
+
+ &:hover {
+ &:before {
+ background-position: 0 -21px;
+ }
+ }
+ }
+
+ li.group ul li > a:before {
+ left: 34px;
+ }
+}
+
+@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
+ .control-filelist.menu-list {
+ li {
+ > a {
+ &:before {
+ background-position: 0px -11px;
+ background-size: 18px auto;
+ }
+
+ &:hover {
+ &:before {
+ background-position: 0px -40px;
+ }
+ }
+ }
+ }
+ }
+
+ .control-filelist.content {
+ li {
+ a {
+ &:before {
+ background-position: 0px -27px;
+ background-size: 17px auto;
+ }
+
+ &:hover {
+ &:before {
+ background-position: 0px -52px;
+ }
+ }
+ }
+ }
+ }
+
+ .control-filelist.snippet-list {
+ li {
+ a {
+ &:before {
+ background-position: 0px -21px;
+ background-size: 17px auto;
+ }
+
+ &:hover {
+ &:before {
+ background-position: 0px -41px;
+ }
+ }
+ }
+ }
+ }
+}
+
+.fancy-layout {
+ .pagesTextEditor {
+ border-left: 1px solid @color-form-field-border!important;
+ }
+}
+
+.control-richeditor {
+ [data-snippet] {
+ &:before {
+ content: attr(data-name);
+ }
+
+ &:after {
+ .page-snippet-icon();
+
+ left: 11px;
+ top: 12px;
+ }
+
+ &.loading:after {
+ background-image:url(../../../../../modules/system/assets/ui/images/loader-transparent.svg);
+ background-size: 15px 15px;
+ background-position: 50% 50%;
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ top: 13px;
+ content: ' ';
+ .animation(spin 1s linear infinite);
+ }
+ }
+}
+
+@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
+ .control-richeditor [data-snippet]:after {
+ background-position: 0px -21px;
+ background-size: 17px auto;
+ }
+}
diff --git a/plugins/rainlab/pages/classes/Content.php b/plugins/rainlab/pages/classes/Content.php
new file mode 100644
index 000000000..776999a28
--- /dev/null
+++ b/plugins/rainlab/pages/classes/Content.php
@@ -0,0 +1,35 @@
+getBaseFileName());
+ $title = ucwords(str_replace(['-', '_'], ' ', $title));
+ return $title;
+ }
+}
diff --git a/plugins/rainlab/pages/classes/Controller.php b/plugins/rainlab/pages/classes/Controller.php
new file mode 100644
index 000000000..a573771e5
--- /dev/null
+++ b/plugins/rainlab/pages/classes/Controller.php
@@ -0,0 +1,123 @@
+theme = Theme::getActiveTheme();
+ if (!$this->theme) {
+ throw new CmsException(Lang::get('cms::lang.theme.active.not_found'));
+ }
+ }
+
+ /**
+ * Creates a CMS page from a static page and configures it.
+ * @param string $url Specifies the static page URL.
+ * @return \Cms\Classes\Page Returns the CMS page object or NULL of the requested page was not found.
+ */
+ public function initCmsPage($url)
+ {
+ $router = new Router($this->theme);
+ $page = $router->findByUrl($url);
+
+ if (!$page) {
+ return null;
+ }
+
+ $viewBag = $page->viewBag;
+
+ $cmsPage = CmsPage::inTheme($this->theme);
+ $cmsPage->url = $url;
+ $cmsPage->apiBag['staticPage'] = $page;
+
+ /*
+ * Transfer specific values from the content view bag to the page settings object.
+ */
+ $viewBagToSettings = ['title', 'layout', 'meta_title', 'meta_description', 'is_hidden'];
+
+ foreach ($viewBagToSettings as $property) {
+ $cmsPage->settings[$property] = array_get($viewBag, $property);
+ }
+
+ // Transer page ID to CMS page
+ $cmsPage->settings['id'] = $page->getId();
+
+ return $cmsPage;
+ }
+
+ public function injectPageTwig($page, $loader, $twig)
+ {
+ if (!isset($page->apiBag['staticPage'])) {
+ return;
+ }
+
+ $staticPage = $page->apiBag['staticPage'];
+
+ CmsException::mask($staticPage, 400);
+ $loader->setObject($staticPage);
+ $template = $twig->loadTemplate($staticPage->getFilePath());
+ $template->render([]);
+ CmsException::unmask();
+ }
+
+ public function getPageContents($page)
+ {
+ if (!isset($page->apiBag['staticPage'])) {
+ return;
+ }
+
+ return $page->apiBag['staticPage']->getProcessedMarkup();
+ }
+
+ public function getPlaceholderContents($page, $placeholderName, $placeholderContents)
+ {
+ if (!isset($page->apiBag['staticPage'])) {
+ return;
+ }
+
+ return $page->apiBag['staticPage']->getProcessedPlaceholderMarkup($placeholderName, $placeholderContents);
+ }
+
+ public function initPageComponents($cmsController, $page)
+ {
+ if (!isset($page->apiBag['staticPage'])) {
+ return;
+ }
+
+ $page->apiBag['staticPage']->initCmsComponents($cmsController);
+ }
+
+ public function parseSyntaxFields($content)
+ {
+ try {
+ return SyntaxParser::parse($content, [
+ 'varPrefix' => 'extraData.',
+ 'tagPrefix' => 'page:'
+ ])->toTwig();
+ }
+ catch (Exception $ex) {
+ return $content;
+ }
+ }
+}
diff --git a/plugins/rainlab/pages/classes/Menu.php b/plugins/rainlab/pages/classes/Menu.php
new file mode 100644
index 000000000..bc0808cd1
--- /dev/null
+++ b/plugins/rainlab/pages/classes/Menu.php
@@ -0,0 +1,300 @@
+ 'required|regex:/^[0-9a-z\-\_]+$/i',
+ ];
+
+ /**
+ * @var array The array of custom error messages.
+ */
+ public $customMessages = [
+ 'required' => 'rainlab.pages::lang.menu.code_required',
+ 'regex' => 'rainlab.pages::lang.menu.invalid_code',
+ ];
+
+ /**
+ * Returns the menu code.
+ * @return string
+ */
+ public function getCodeAttribute()
+ {
+ return $this->getBaseFileName();
+ }
+
+ /**
+ * Sets the menu code.
+ * @param string $code Specifies the file code.
+ * @return \Cms\Classes\CmsObject Returns the object instance.
+ */
+ public function setCodeAttribute($code)
+ {
+ $code = trim($code);
+
+ if (strlen($code)) {
+ $this->fileName = $code.'.yaml';
+ $this->attributes = array_merge($this->attributes, ['code' => $code]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns a default value for items attribute.
+ * Items are objects of the \RainLab\Pages\Classes\MenuItem class.
+ * @return array
+ */
+ public function getItemsAttribute()
+ {
+ $items = [];
+ if (!empty($this->attributes['items'])) {
+ $items = MenuItem::initFromArray($this->attributes['items']);
+ }
+
+ return $items;
+ }
+
+ /**
+ * Store the itemData in the items attribute
+ *
+ * @param array $data
+ * @return void
+ */
+ public function setItemDataAttribute($data)
+ {
+ $this->items = $data;
+ return $this;
+ }
+
+ /**
+ * Processes the content attribute to an array of menu data.
+ * @return array|null
+ */
+ protected function parseContent()
+ {
+ $parsedData = parent::parseContent();
+
+ if (!array_key_exists('name', $parsedData)) {
+ throw new SystemException(sprintf('The content of the %s file is invalid: the name element is not found.', $this->fileName));
+ }
+
+ return $parsedData;
+ }
+
+ /**
+ * Initializes a cache item.
+ * @param array &$item The cached item array.
+ */
+ public static function initCacheItem(&$item)
+ {
+ $obj = new static($item);
+ $item['name'] = $obj->name;
+ $item['items'] = $obj->items;
+ }
+
+ /**
+ * Returns the menu item references.
+ * This function is used on the front-end.
+ * @param Cms\Classes\Page $page The current page object.
+ * @return array Returns an array of the \RainLab\Pages\Classes\MenuItemReference objects.
+ */
+ public function generateReferences($page)
+ {
+ $currentUrl = Request::path();
+
+ if (!strlen($currentUrl)) {
+ $currentUrl = '/';
+ }
+
+ $currentUrl = Str::lower(Url::to($currentUrl));
+
+ $activeMenuItem = $page->activeMenuItem ?: false;
+ $iterator = function($items) use ($currentUrl, &$iterator, $activeMenuItem) {
+ $result = [];
+
+ foreach ($items as $item) {
+ $parentReference = new MenuItemReference;
+ $parentReference->type = $item->type;
+ $parentReference->title = $item->title;
+ $parentReference->code = $item->code;
+ $parentReference->viewBag = $item->viewBag;
+
+ /*
+ * If the item type is URL, assign the reference the item's URL and compare the current URL with the item URL
+ * to determine whether the item is active.
+ */
+ if ($item->type == 'url') {
+ $parentReference->url = $item->url;
+ $parentReference->isActive = $currentUrl == Str::lower(Url::to($item->url)) || $activeMenuItem === $item->code;
+ }
+ else {
+ /*
+ * If the item type is not URL, use the API to request the item type's provider to
+ * return the item URL, subitems and determine whether the item is active.
+ */
+ $apiResult = Event::fire('pages.menuitem.resolveItem', [$item->type, $item, $currentUrl, $this->theme]);
+ if (is_array($apiResult)) {
+ foreach ($apiResult as $itemInfo) {
+ if (!is_array($itemInfo)) {
+ continue;
+ }
+
+ if (!$item->replace && isset($itemInfo['url'])) {
+ $parentReference->url = $itemInfo['url'];
+ $parentReference->isActive = $itemInfo['isActive'] || $activeMenuItem === $item->code;
+ }
+
+ if (isset($itemInfo['items'])) {
+ $itemIterator = function($items) use (&$itemIterator, $parentReference) {
+ $result = [];
+
+ foreach ($items as $item) {
+ $reference = new MenuItemReference;
+ $reference->type = isset($item['type']) ? $item['type'] : null;
+ $reference->title = isset($item['title']) ? $item['title'] : '--no title--';
+ $reference->url = isset($item['url']) ? $item['url'] : '#';
+ $reference->isActive = isset($item['isActive']) ? $item['isActive'] : false;
+ $reference->viewBag = isset($item['viewBag']) ? $item['viewBag'] : [];
+ $reference->code = isset($item['code']) ? $item['code'] : null;
+
+ if (!strlen($parentReference->url)) {
+ $parentReference->url = $reference->url;
+ $parentReference->isActive = $reference->isActive;
+ }
+
+ if (isset($item['items'])) {
+ $reference->items = $itemIterator($item['items']);
+ }
+
+ $result[] = $reference;
+ }
+
+ return $result;
+ };
+
+ $parentReference->items = $itemIterator($itemInfo['items']);
+ }
+ }
+ }
+ }
+
+ if ($item->items) {
+ $parentReference->items = $iterator($item->items);
+ }
+
+ if (!$item->replace) {
+ $result[] = $parentReference;
+ }
+ else {
+ foreach ($parentReference->items as $subItem) {
+ $result[] = $subItem;
+ }
+ }
+ }
+
+ return $result;
+ };
+
+ $items = $iterator($this->items);
+
+ /*
+ * Populate the isChildActive property
+ */
+ $hasActiveChild = function($items) use (&$hasActiveChild) {
+ foreach ($items as $item) {
+ if ($item->isActive) {
+ return true;
+ }
+
+ $result = $hasActiveChild($item->items);
+ if ($result) {
+ return $result;
+ }
+ }
+ };
+
+ $iterator = function($items) use (&$iterator, &$hasActiveChild) {
+ foreach ($items as $item) {
+ $item->isChildActive = $hasActiveChild($item->items);
+
+ $iterator($item->items);
+ }
+ };
+
+ $iterator($items);
+
+ /*
+ * @event pages.menu.referencesGenerated
+ * Provides opportunity to dynamically change menu entries right after reference generation.
+ *
+ * For example you can use it to filter menu entries for user groups from RainLab.User
+ * Before doing so you have to add custom field 'group' to menu viewBag using backend.form.extendFields event
+ * where the group can be selected by the user. See how to do this here:
+ * https://octobercms.com/docs/backend/forms#extend-form-fields
+ *
+ * Parameter provided is `$items` - a collection of the MenuItemReference objects passed by reference
+ *
+ * For example to hide entries where group is not 'registered' you can use the following code. It can
+ * be used to show different menus for different user groups.
+ *
+ * Event::listen('pages.menu.referencesGenerated', function (&$items) {
+ * $iterator = function ($menuItems) use (&$iterator, $clusterRepository) {
+ * $result = [];
+ * foreach ($menuItems as $item) {
+ * if (isset($item->viewBag['group']) && $item->viewBag['group'] !== "registered") {
+ * $item->viewBag['isHidden'] = "1";
+ * }
+ * if ($item->items) {
+ * $item->items = $iterator($item->items);
+ * }
+ * $result[] = $item;
+ * }
+ * return $result;
+ * };
+ * $items = $iterator($items);
+ * });
+ */
+ Event::fire('pages.menu.referencesGenerated', [&$items]);
+
+ return $items;
+ }
+}
diff --git a/plugins/rainlab/pages/classes/MenuItem.php b/plugins/rainlab/pages/classes/MenuItem.php
new file mode 100644
index 000000000..5734e9e42
--- /dev/null
+++ b/plugins/rainlab/pages/classes/MenuItem.php
@@ -0,0 +1,214 @@
+ $value) {
+ if ($name != 'items') {
+ if (property_exists($obj, $name)) {
+ $obj->$name = $value;
+ }
+ }
+ else {
+ $obj->items = self::initFromArray($value);
+ }
+ }
+
+ $result[] = $obj;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a list of registered menu item types
+ * @return array Returns an array of registered item types
+ */
+ public function getTypeOptions($keyValue = null)
+ {
+ /*
+ * Baked in types
+ */
+ $result = [
+ 'url' => 'URL',
+ 'header' => 'Header',
+ ];
+
+ $apiResult = Event::fire('pages.menuitem.listTypes');
+
+ if (is_array($apiResult)) {
+ foreach ($apiResult as $typeList) {
+ if (!is_array($typeList)) {
+ continue;
+ }
+
+ foreach ($typeList as $typeCode => $typeName) {
+ $result[$typeCode] = $typeName;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ public function getCmsPageOptions($keyValue = null)
+ {
+ return []; // CMS Pages are loaded client-side
+ }
+
+ public function getReferenceOptions($keyValue = null)
+ {
+ return []; // References are loaded client-side
+ }
+
+ public static function getTypeInfo($type)
+ {
+ $result = [];
+ $apiResult = Event::fire('pages.menuitem.getTypeInfo', [$type]);
+
+ if (is_array($apiResult)) {
+ foreach ($apiResult as $typeInfo) {
+ if (!is_array($typeInfo)) {
+ continue;
+ }
+
+ foreach ($typeInfo as $name => $value) {
+ if ($name == 'cmsPages') {
+ $cmsPages = [];
+
+ foreach ($value as $page) {
+ $baseName = $page->getBaseFileName();
+ $pos = strrpos($baseName, '/');
+
+ $dir = $pos !== false ? substr($baseName, 0, $pos).' / ' : null;
+ $cmsPages[$baseName] = strlen($page->title)
+ ? $dir.$page->title
+ : $baseName;
+ }
+
+ $value = $cmsPages;
+ }
+
+ $result[$name] = $value;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Converts the menu item data to an array
+ * @return array Returns the menu item data as array
+ */
+ public function toArray()
+ {
+ $result = [];
+
+ foreach ($this->fillable as $property) {
+ $result[$property] = $this->$property;
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/rainlab/pages/classes/MenuItemReference.php b/plugins/rainlab/pages/classes/MenuItemReference.php
new file mode 100644
index 000000000..7ee4b7a0a
--- /dev/null
+++ b/plugins/rainlab/pages/classes/MenuItemReference.php
@@ -0,0 +1,58 @@
+ 'required',
+ 'url' => ['required', 'regex:/^\/[a-z0-9\/_\-\.]*$/i', 'uniqueUrl']
+ ];
+
+ /**
+ * @var array The array of custom attribute names.
+ */
+ public $attributeNames = [
+ 'title' => 'title',
+ 'url' => 'url',
+ ];
+
+ /**
+ * @var array Attributes that support translation, if available.
+ */
+ public $translatable = [
+ 'code',
+ 'markup',
+ 'viewBag[title]',
+ 'viewBag[meta_title]',
+ 'viewBag[meta_description]',
+ ];
+
+ /**
+ * @var string Translation model used for translation, if available.
+ */
+ public $translatableModel = 'RainLab\Translate\Classes\MLStaticPage';
+
+ /**
+ * @var string Contains the page parent file name.
+ * This property is used by the page editor internally.
+ */
+ public $parentFileName;
+
+ protected static $menuTreeCache = null;
+
+ protected $parentCache = null;
+
+ protected $childrenCache = null;
+
+ protected $processedMarkupCache = false;
+
+ protected $processedBlockMarkupCache = [];
+
+ /**
+ * Creates an instance of the object and associates it with a CMS theme.
+ * @param array $attributes
+ */
+ public function __construct(array $attributes = [])
+ {
+ parent::__construct($attributes);
+
+ $this->customMessages = [
+ 'url.regex' => 'rainlab.pages::lang.page.invalid_url',
+ 'url.unique_url' => 'rainlab.pages::lang.page.url_not_unique',
+ ];
+ }
+
+ //
+ // CMS Object
+ //
+
+ /**
+ * Sets the object attributes.
+ * @param array $attributes A list of attributes to set.
+ */
+ public function fill(array $attributes)
+ {
+ parent::fill($attributes);
+
+ /*
+ * When the page is saved, copy setting properties to the view bag.
+ * This is required for the back-end editors.
+ */
+ if (array_key_exists('settings', $attributes) && array_key_exists('viewBag', $attributes['settings'])) {
+ $this->getViewBag()->setProperties($attributes['settings']['viewBag']);
+ $this->fillViewBagArray();
+ }
+ }
+
+ /**
+ * Returns the attributes used for validation.
+ * @return array
+ */
+ protected function getValidationAttributes()
+ {
+ return $this->getAttributes() + $this->viewBag;
+ }
+
+ /**
+ * Validates the object properties.
+ * Throws a ValidationException in case of an error.
+ */
+ public function beforeValidate()
+ {
+ $pages = Page::listInTheme($this->theme, true);
+
+ Validator::extend('uniqueUrl', function($attribute, $value, $parameters) use ($pages) {
+ $value = trim(strtolower($value));
+
+ foreach ($pages as $existingPage) {
+ if (
+ $existingPage->getBaseFileName() !== $this->getBaseFileName() &&
+ strtolower($existingPage->getViewBag()->property('url')) == $value
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }
+
+ /**
+ * Triggered before a new object is saved.
+ */
+ public function beforeCreate()
+ {
+ $this->fileName = $this->generateFilenameFromCode();
+ }
+
+ /**
+ * Triggered after a new object is saved.
+ */
+ public function afterCreate()
+ {
+ $this->appendToMeta();
+ }
+
+ /**
+ * Adds this page to the meta index.
+ */
+ protected function appendToMeta()
+ {
+ $pageList = new PageList($this->theme);
+ $pageList->appendPage($this);
+ }
+
+ /*
+ * Generate a file name based on the URL
+ */
+ protected function generateFilenameFromCode()
+ {
+ $dir = rtrim($this->getFilePath(''), '/');
+
+ $fileName = trim(str_slug(str_replace('/', '-', $this->getViewBag()->property('url')), '-'));
+ if (strlen($fileName) > 200) {
+ $fileName = substr($fileName, 0, 200);
+ }
+
+ if (!strlen($fileName)) {
+ $fileName = 'index';
+ }
+
+ $curName = trim($fileName).'.htm';
+ $counter = 2;
+
+ while (File::exists($dir.'/'.$curName)) {
+ $curName = $fileName.'-'.$counter.'.htm';
+ $counter++;
+ }
+
+ return $curName;
+ }
+
+ /**
+ * Deletes the object from the disk.
+ * Recursively deletes subpages. Returns a list of file names of deleted pages.
+ * @return array
+ */
+ public function delete()
+ {
+ $result = [];
+
+ /*
+ * Delete subpages
+ */
+ foreach ($this->getChildren() as $subPage) {
+ $result = array_merge($result, $subPage->delete());
+ }
+
+ /*
+ * Delete the object
+ */
+ $result = array_merge($result, [$this->getBaseFileName()]);
+
+ parent::delete();
+
+ /*
+ * Remove from meta
+ */
+ $this->removeFromMeta();
+
+
+ return $result;
+ }
+
+ /**
+ * Removes this page to the meta index.
+ */
+ protected function removeFromMeta()
+ {
+ $pageList = new PageList($this->theme);
+ $pageList->removeSubtree($this);
+ }
+
+ //
+ // Public API
+ //
+
+ /**
+ * Helper that makes a URL for a static page in the active theme.
+ *
+ * Guide for the page reference:
+ * - chairs -> content/static-pages/chairs.htm
+ *
+ * @param mixed $page Specifies the Content file name.
+ * @return string
+ */
+ public static function url($name)
+ {
+ if (empty($name) || !$page = static::find($name)) {
+ return null;
+ }
+
+ $url = array_get($page->attributes, 'viewBag.url');
+
+ return Cms::url($url);
+ }
+
+ /**
+ * Determine the default layout for a new page
+ * @param \RainLab\Pages\Classes\Page $parentPage
+ */
+ public function setDefaultLayout($parentPage)
+ {
+ // Check parent page for a defined child layout
+ if ($parentPage && $parentPage->layout) {
+ $layout = Layout::load($this->theme, $parentPage->layout);
+ $component = $layout ? $layout->getComponent('staticPage') : null;
+ $childLayoutName = $component ? $component->property('childLayout', null) : null;
+ if ($childLayoutName) {
+ $this->getViewBag()->setProperty('layout', $childLayoutName);
+ $this->fillViewBagArray();
+ return;
+ }
+ }
+
+ // Check theme layouts for one marked as the default
+ foreach (Layout::listInTheme($this->theme) as $layout) {
+ $component = $layout->getComponent('staticPage');
+ if ($component && $component->property('default', false)) {
+ $this->getViewBag()->setProperty('layout', $layout->getBaseFileName());
+ $this->fillViewBagArray();
+ return;
+ }
+ }
+ }
+
+ //
+ // Getters
+ //
+
+ /**
+ * Returns the parent page that belongs to this one, or null.
+ * @return mixed
+ */
+ public function getParent()
+ {
+ if ($this->parentCache !== null) {
+ return $this->parentCache;
+ }
+
+ $pageList = new PageList($this->theme);
+
+ $parent = null;
+ if ($fileName = $pageList->getPageParent($this)) {
+ $parent = static::load($this->theme, $fileName);
+ }
+
+ return $this->parentCache = $parent;
+ }
+
+ /**
+ * Returns all the child pages that belong to this one.
+ * @return array
+ */
+ public function getChildren()
+ {
+ if ($this->childrenCache !== null) {
+ return $this->childrenCache;
+ }
+
+ $children = [];
+ $pageList = new PageList($this->theme);
+
+ $subtree = $pageList->getPageSubTree($this);
+
+ foreach ($subtree as $fileName => $subPages) {
+ $subPage = static::load($this->theme, $fileName);
+ if ($subPage) {
+ $children[] = $subPage;
+ }
+ }
+
+ return $this->childrenCache = $children;
+ }
+
+ /**
+ * Returns a list of layouts available in the theme.
+ * This method is used by the form widget.
+ * @return array Returns an array of strings.
+ */
+ public function getLayoutOptions()
+ {
+ $result = [];
+ $layouts = Layout::listInTheme($this->theme, true);
+
+ foreach ($layouts as $layout) {
+ if (!$layout->hasComponent('staticPage')) {
+ continue;
+ }
+
+ $baseName = $layout->getBaseFileName();
+ $result[$baseName] = strlen($layout->description) ? $layout->description : $baseName;
+ }
+
+ if (!$result) {
+ $result[null] = Lang::get('rainlab.pages::lang.page.layouts_not_found');
+ }
+
+ return $result;
+ }
+
+ /**
+ * Looks up the Layout Cms object for this page.
+ * @return Cms\Classes\Layout
+ */
+ public function getLayoutObject()
+ {
+ $viewBag = $this->getViewBag();
+ $layout = $viewBag->property('layout');
+
+ if (!$layout) {
+ $layouts = $this->getLayoutOptions();
+ $layout = count($layouts) ? array_keys($layouts)[0] : null;
+ }
+
+ if (!$layout) {
+ return null;
+ }
+
+ $layout = Layout::load($this->theme, $layout);
+ if (!$layout) {
+ return null;
+ }
+
+ return $layout;
+ }
+
+ /**
+ * Returns the Twig content string
+ */
+ public function getTwigContent()
+ {
+ return $this->code;
+ }
+
+ //
+ // Syntax field processing
+ //
+
+ public function listLayoutSyntaxFields()
+ {
+ if (!$layout = $this->getLayoutObject()) {
+ return [];
+ }
+
+ $syntax = SyntaxParser::parse($layout->markup, ['tagPrefix' => 'page:']);
+ $result = $syntax->toEditor();
+
+ return $result;
+ }
+
+ //
+ // Placeholder processing
+ //
+
+ /**
+ * Returns information about placeholders defined in the page layout.
+ * @return array Returns an associative array of the placeholder name and codes.
+ */
+ public function listLayoutPlaceholders()
+ {
+ if (!$layout = $this->getLayoutObject()) {
+ return [];
+ }
+
+ $result = [];
+ $bodyNode = $layout->getTwigNodeTree()->getNode('body')->getNode(0);
+ $nodes = $this->flattenTwigNode($bodyNode);
+
+ foreach ($nodes as $node) {
+ if (!$node instanceof \Cms\Twig\PlaceholderNode) {
+ continue;
+ }
+
+ $title = $node->hasAttribute('title') ? trim($node->getAttribute('title')) : null;
+ if (!strlen($title)) {
+ $title = $node->getAttribute('name');
+ }
+
+ $type = $node->hasAttribute('type') ? trim($node->getAttribute('type')) : null;
+ $ignore = $node->hasAttribute('ignore') ? trim($node->getAttribute('ignore')) : false;
+
+ $placeholderInfo = [
+ 'title' => $title,
+ 'type' => $type ?: 'html',
+ 'ignore' => $ignore
+ ];
+
+ $result[$node->getAttribute('name')] = $placeholderInfo;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Recursively flattens a twig node and children
+ * @param $node
+ * @return array A flat array of twig nodes
+ */
+ protected function flattenTwigNode($node)
+ {
+ $result = [];
+ if (!$node instanceof TwigNode) {
+ return $result;
+ }
+
+ foreach ($node as $subNode) {
+ $flatNodes = $this->flattenTwigNode($subNode);
+ $result = array_merge($result, [$subNode], $flatNodes);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Parses the page placeholder {% put %} tags and extracts the placeholder values.
+ * @return array Returns an associative array of the placeholder names and values.
+ */
+ public function getPlaceholdersAttribute()
+ {
+ if (!strlen($this->code)) {
+ return [];
+ }
+
+ if ($placeholders = array_get($this->attributes, 'placeholders')) {
+ return $placeholders;
+ }
+
+ $bodyNode = $this->getTwigNodeTree($this->code)->getNode('body')->getNode(0);
+ if ($bodyNode instanceof \Cms\Twig\PutNode) {
+ $bodyNode = [$bodyNode];
+ }
+
+ $result = [];
+ foreach ($bodyNode as $node) {
+ if (!$node instanceof \Cms\Twig\PutNode) {
+ continue;
+ }
+
+ $bodyNode = $node->getNode('body');
+ $result[$node->getAttribute('name')] = trim($bodyNode->getAttribute('data'));
+ }
+
+ $this->attributes['placeholders'] = $result;
+
+ return $result;
+ }
+
+ /**
+ * Takes an array of placeholder data (key: code, value: content) and renders
+ * it as a single string of Twig markup against the "code" attribute.
+ * @param array $value
+ * @return void
+ */
+ public function setPlaceholdersAttribute($value)
+ {
+ if (!is_array($value)) {
+ return;
+ }
+
+ // Prune any attempt at setting a placeholder that
+ // is not actually defined by this pages layout.
+ $placeholders = array_intersect_key($value, $this->listLayoutPlaceholders());
+
+ $result = '';
+
+ foreach ($placeholders as $code => $content) {
+ if (!strlen(trim($content))) {
+ continue;
+ }
+
+ $result .= '{% put '.$code.' %}'.PHP_EOL;
+ $result .= $content.PHP_EOL;
+ $result .= '{% endput %}'.PHP_EOL;
+ $result .= PHP_EOL;
+ }
+
+ $this->attributes['code'] = trim($result);
+ $this->attributes['placeholders'] = $placeholders;
+ }
+
+ public function getProcessedMarkup()
+ {
+ if ($this->processedMarkupCache !== false) {
+ return $this->processedMarkupCache;
+ }
+
+ /*
+ * Process snippets
+ */
+ $markup = Snippet::processPageMarkup(
+ $this->getFileName(),
+ $this->theme,
+ $this->markup
+ );
+
+ /*
+ * Inject global view variables
+ */
+ $globalVars = ViewHelper::getGlobalVars();
+ if (!empty($globalVars)) {
+ $markup = TextParser::parse($markup, $globalVars);
+ }
+
+ return $this->processedMarkupCache = $markup;
+ }
+
+ public function getProcessedPlaceholderMarkup($placeholderName, $placeholderContents)
+ {
+ if (array_key_exists($placeholderName, $this->processedBlockMarkupCache)) {
+ return $this->processedBlockMarkupCache[$placeholderName];
+ }
+
+ /*
+ * Process snippets
+ */
+ $markup = Snippet::processPageMarkup(
+ $this->getFileName().md5($placeholderName),
+ $this->theme,
+ $placeholderContents
+ );
+
+ /*
+ * Inject global view variables
+ */
+ $globalVars = ViewHelper::getGlobalVars();
+ if (!empty($globalVars)) {
+ $markup = TextParser::parse($markup, $globalVars);
+ }
+
+ return $this->processedBlockMarkupCache[$placeholderName] = $markup;
+ }
+
+ //
+ // Snippets
+ //
+
+ /**
+ * Initializes CMS components associated with the page.
+ */
+ public function initCmsComponents($cmsController)
+ {
+ $snippetComponents = Snippet::listPageComponents(
+ $this->getFileName(),
+ $this->theme,
+ $this->markup.$this->code
+ );
+
+ $componentManager = ComponentManager::instance();
+ foreach ($snippetComponents as $componentInfo) {
+ // Register components for snippet-based components
+ // if they're not defined yet. This is required because
+ // not all snippet components are registered as components,
+ // but it's safe to register them in render-time.
+
+ if (!$componentManager->hasComponent($componentInfo['class'])) {
+ $componentManager->registerComponent($componentInfo['class'], $componentInfo['alias']);
+ }
+
+ $cmsController->addComponent(
+ $componentInfo['class'],
+ $componentInfo['alias'],
+ $componentInfo['properties']
+ );
+ }
+ }
+
+ //
+ // Static Menu API
+ //
+
+ /**
+ * Returns a cache key for this record.
+ */
+ protected static function getMenuCacheKey($theme)
+ {
+ $key = crc32($theme->getPath()).'static-page-menu';
+ /**
+ * @event pages.page.getMenuCacheKey
+ * Enables modifying the key used to reference cached RainLab.Pages menu trees
+ *
+ * Example usage:
+ *
+ * Event::listen('pages.page.getMenuCacheKey', function (&$key) {
+ * $key = $key . '-' . App::getLocale();
+ * });
+ *
+ */
+ Event::fire('pages.page.getMenuCacheKey', [&$key]);
+ return $key;
+ }
+
+ /**
+ * Returns whether the specified URLs are equal.
+ */
+ protected static function urlsAreEqual($url, $other)
+ {
+ return rawurldecode($url) === rawurldecode($other);
+ }
+
+ /**
+ * Clears the menu item cache
+ * @param \Cms\Classes\Theme $theme Specifies the current theme.
+ */
+ public static function clearMenuCache($theme)
+ {
+ Cache::forget(self::getMenuCacheKey($theme));
+ }
+
+ /**
+ * Handler for the pages.menuitem.getTypeInfo event.
+ * Returns a menu item type information. The type information is returned as array
+ * with the following elements:
+ * - references - a list of the item type reference options. The options are returned in the
+ * ["key"] => "title" format for options that don't have sub-options, and in the format
+ * ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
+ * required only if the menu item type requires references.
+ * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
+ * false if omitted.
+ * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
+ * Optional, false if omitted.
+ * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
+ * resolve the item URL.
+ * @param string $type Specifies the menu item type
+ * @return array Returns an array
+ */
+ public static function getMenuTypeInfo($type)
+ {
+ if ($type == 'all-static-pages') {
+ return [
+ 'dynamicItems' => true
+ ];
+ }
+
+ if ($type == 'static-page') {
+ return [
+ 'references' => self::listStaticPageMenuOptions(),
+ 'nesting' => true,
+ 'dynamicItems' => true
+ ];
+ }
+ }
+
+ /**
+ * Handler for the pages.menuitem.resolveItem event.
+ * Returns information about a menu item. The result is an array
+ * with the following keys:
+ * - url - the menu item URL. Not required for menu item types that return all available records.
+ * The URL should be returned relative to the website root and include the subdirectory, if any.
+ * Use the Cms::url() helper to generate the URLs.
+ * - isActive - determines whether the menu item is active. Not required for menu item types that
+ * return all available records.
+ * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
+ * The items array should be added only if the $item's $nesting property value is TRUE.
+ * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
+ * @param \Cms\Classes\Theme $theme Specifies the current theme.
+ * @param string $url Specifies the current page URL, normalized, in lower case
+ * The URL is specified relative to the website root, it includes the subdirectory name, if any.
+ * @return mixed Returns an array. Returns null if the item cannot be resolved.
+ */
+ public static function resolveMenuItem($item, $url, $theme)
+ {
+ $tree = self::buildMenuTree($theme);
+
+ if ($item->type == 'static-page' && !isset($tree[$item->reference])) {
+ return;
+ }
+
+ $result = [];
+
+ if ($item->type == 'static-page') {
+ $pageInfo = $tree[$item->reference];
+ $result['url'] = Cms::url($pageInfo['url']);
+ $result['mtime'] = $pageInfo['mtime'];
+ $result['isActive'] = self::urlsAreEqual($result['url'], $url);
+ }
+
+ if ($item->nesting || $item->type == 'all-static-pages') {
+ $iterator = function($items) use (&$iterator, &$tree, $url) {
+ $branch = [];
+
+ foreach ($items as $itemName) {
+ if (!isset($tree[$itemName])) {
+ continue;
+ }
+
+ $itemInfo = $tree[$itemName];
+
+ if ($itemInfo['navigation_hidden']) {
+ continue;
+ }
+
+ $branchItem = [];
+ $branchItem['url'] = Cms::url($itemInfo['url']);
+ $branchItem['isActive'] = self::urlsAreEqual($branchItem['url'], $url);
+ $branchItem['title'] = $itemInfo['title'];
+ $branchItem['mtime'] = $itemInfo['mtime'];
+
+ if ($itemInfo['items']) {
+ $branchItem['items'] = $iterator($itemInfo['items']);
+ }
+
+ $branch[] = $branchItem;
+ }
+
+ return $branch;
+ };
+
+ $result['items'] = $iterator($item->type == 'static-page' ? $pageInfo['items'] : $tree['--root-pages--']);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Handler for the backend.richeditor.getTypeInfo event.
+ * Returns a menu item type information. The type information is returned as array
+ *
+ * @param string $type Specifies the page link type
+ * @return array Array of available link targets keyed by URL ['https://example.com/' => 'Homepage]
+ */
+ public static function getRichEditorTypeInfo($type)
+ {
+ if ($type == 'static-page') {
+
+ $pages = self::listStaticPageMenuOptions();
+
+ $iterator = function($pages) use (&$iterator) {
+ $result = [];
+ foreach ($pages as $pageFile => $page) {
+ $url = self::url($pageFile);
+
+ if (is_array($page)) {
+ $result[$url] = [
+ 'title' => array_get($page, 'title', []),
+ 'links' => $iterator(array_get($page, 'items', []))
+ ];
+ }
+ else {
+ $result[$url] = $page;
+ }
+ }
+
+ return $result;
+ };
+
+ return $iterator($pages);
+ }
+
+ return [];
+ }
+
+ /**
+ * Builds and caches a menu item tree.
+ * This method is used internally for menu items and breadcrumbs.
+ * @param \Cms\Classes\Theme $theme Specifies the current theme.
+ * @return array Returns an array containing the page information
+ */
+ public static function buildMenuTree($theme)
+ {
+ if (self::$menuTreeCache !== null) {
+ return self::$menuTreeCache;
+ }
+
+ $key = self::getMenuCacheKey($theme);
+
+ $cached = Cache::get($key, false);
+ $unserialized = $cached ? @unserialize($cached) : false;
+
+ if ($unserialized !== false) {
+ return self::$menuTreeCache = $unserialized;
+ }
+
+ $menuTree = [
+ '--root-pages--' => []
+ ];
+
+ $iterator = function($items, $parent, $level) use (&$menuTree, &$iterator) {
+ $result = [];
+
+ foreach ($items as $item) {
+ $viewBag = $item->page->viewBag;
+ $pageCode = $item->page->getBaseFileName();
+ $pageUrl = Str::lower(RouterHelper::normalizeUrl(array_get($viewBag, 'url')));
+
+ $itemData = [
+ 'url' => $pageUrl,
+ 'title' => array_get($viewBag, 'title'),
+ 'mtime' => $item->page->mtime,
+ 'items' => $iterator($item->subpages, $pageCode, $level+1),
+ 'parent' => $parent,
+ 'navigation_hidden' => array_get($viewBag, 'navigation_hidden')
+ ];
+
+ if ($level == 0) {
+ $menuTree['--root-pages--'][] = $pageCode;
+ }
+
+ $result[] = $pageCode;
+ $menuTree[$pageCode] = $itemData;
+ }
+
+ return $result;
+ };
+
+ $pageList = new PageList($theme);
+ $iterator($pageList->getPageTree(), null, 0);
+
+ self::$menuTreeCache = $menuTree;
+ $comboConfig = Config::get('cms.parsedPageCacheTTL', Config::get('cms.template_cache_ttl', 10));
+ $expiresAt = now()->addMinutes($comboConfig);
+ Cache::put($key, serialize($menuTree), $expiresAt);
+
+ return self::$menuTreeCache;
+ }
+
+ /**
+ * Returns a list of options for the Reference drop-down menu in the
+ * menu item configuration form, when the Static Page item type is selected.
+ * @return array Returns an array
+ */
+ protected static function listStaticPageMenuOptions()
+ {
+ $theme = Theme::getEditTheme();
+
+ $pageList = new PageList($theme);
+ $pageTree = $pageList->getPageTree(true);
+
+ $iterator = function($pages) use (&$iterator) {
+ $result = [];
+
+ foreach ($pages as $pageInfo) {
+ $pageName = $pageInfo->page->getViewBag()->property('title');
+ $fileName = $pageInfo->page->getBaseFileName();
+
+ if (!$pageInfo->subpages) {
+ $result[$fileName] = $pageName;
+ }
+ else {
+ $result[$fileName] = [
+ 'title' => $pageName,
+ 'items' => $iterator($pageInfo->subpages)
+ ];
+ }
+ }
+
+ return $result;
+ };
+
+ return $iterator($pageTree);
+ }
+
+ /**
+ * Disables safe mode check for static pages.
+ *
+ * This allows developers to use placeholders in layouts even if safe mode is enabled.
+ *
+ * @return void
+ */
+ protected function checkSafeMode()
+ {
+ }
+}
diff --git a/plugins/rainlab/pages/classes/PageList.php b/plugins/rainlab/pages/classes/PageList.php
new file mode 100644
index 000000000..547a2fe53
--- /dev/null
+++ b/plugins/rainlab/pages/classes/PageList.php
@@ -0,0 +1,236 @@
+theme = $theme;
+ }
+
+ /**
+ * Returns a list of static pages in the specified theme.
+ * This method is used internally by the system.
+ * @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache.
+ * @return array Returns an array of static pages.
+ */
+ public function listPages($skipCache = false)
+ {
+ return Page::listInTheme($this->theme, $skipCache);
+ }
+
+ /**
+ * Returns a list of top-level pages with subpages.
+ * The method uses the theme's meta/static-pages.yaml file to build the hierarchy. The pages are returned
+ * in the order defined in the YAML file. The result of the method is used for building the back-end UI
+ * and for generating the menus.
+ * @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache.
+ * @return array Returns a nested array of objects: object('page': $pageObj, 'subpages'=>[...])
+ */
+ public function getPageTree($skipCache = false)
+ {
+ $pages = $this->listPages($skipCache);
+ $config = $this->getPagesConfig();
+
+ // Make the $pages collection an associative array for performance
+ $pagesArray = $pages->keyBy(function ($page) {
+ return $page->getBaseFileName();
+ })->all();
+
+ $iterator = function($configPages) use (&$iterator, $pagesArray) {
+ $result = [];
+
+ foreach ($configPages as $fileName => $subpages) {
+ if (isset($pagesArray[$fileName])) {
+ $result[] = (object) [
+ 'page' => $pagesArray[$fileName],
+ 'subpages' => $iterator($subpages),
+ ];
+ }
+ }
+
+ return $result;
+ };
+
+ return $iterator($config['static-pages']);
+ }
+
+ /**
+ * Returns the parent name of the specified page.
+ * @param \Cms\Classes\Page $page Specifies a page object.
+ * @param string Returns the parent page name.
+ */
+ public function getPageParent($page)
+ {
+ $pagesConfig = $this->getPagesConfig();
+ $requestedFileName = $page->getBaseFileName();
+
+ $parent = null;
+
+ $iterator = function($configPages) use (&$iterator, &$parent, $requestedFileName) {
+ foreach ($configPages as $fileName => $subpages) {
+ if ($fileName == $requestedFileName) {
+ return true;
+ }
+
+ if ($iterator($subpages) == true && is_null($parent)) {
+
+ $parent = $fileName;
+
+ return true;
+ }
+ }
+ };
+
+ $iterator($pagesConfig['static-pages']);
+
+ return $parent;
+ }
+
+ /**
+ * Returns a part of the page hierarchy starting from the specified page.
+ * @param \Cms\Classes\Page $page Specifies a page object.
+ * @param array Returns a nested array of page names.
+ */
+ public function getPageSubTree($page)
+ {
+ $pagesConfig = $this->getPagesConfig();
+ $requestedFileName = $page->getBaseFileName();
+
+ $subTree = [];
+
+ $iterator = function($configPages) use (&$iterator, &$subTree, $requestedFileName) {
+ if (is_array($configPages)) {
+ foreach ($configPages as $fileName => $subpages) {
+ if ($fileName == $requestedFileName) {
+ $subTree = $subpages;
+
+ return true;
+ }
+
+ if ($iterator($subpages) === true) {
+ return true;
+ }
+ }
+ }
+ };
+
+ $iterator($pagesConfig['static-pages']);
+
+ return $subTree;
+ }
+
+ /**
+ * Appends page to the page hierarchy.
+ * The page can be added to the end of the hierarchy or as a subpage to any existing page.
+ */
+ public function appendPage($page)
+ {
+ $parent = $page->parentFileName;
+
+ $originalData = $this->getPagesConfig();
+ $structure = $originalData['static-pages'];
+
+ if (!strlen($parent)) {
+ $structure[$page->getBaseFileName()] = [];
+ }
+ else {
+ $iterator = function(&$configPages) use (&$iterator, $parent, $page) {
+ foreach ($configPages as $fileName => &$subpages) {
+ if ($fileName == $parent) {
+ $subpages[$page->getBaseFileName()] = [];
+
+ return true;
+ }
+
+ if ($iterator($subpages) == true)
+ return true;
+ }
+ };
+
+ $iterator($structure);
+ }
+
+ $this->updateStructure($structure);
+ }
+
+ /**
+ * Removes a part of the page hierarchy starting from the specified page.
+ * @param \Cms\Classes\Page $page Specifies a page object.
+ */
+ public function removeSubtree($page)
+ {
+ $pagesConfig = $this->getPagesConfig();
+ $requestedFileName = $page->getBaseFileName();
+
+ $tree = [];
+
+ $iterator = function($configPages) use (&$iterator, &$pages, $requestedFileName) {
+ $result = [];
+
+ foreach ($configPages as $fileName => $subpages) {
+ if ($requestedFileName != $fileName) {
+ $result[$fileName] = $iterator($subpages);
+ }
+ }
+
+ return $result;
+ };
+
+ $updatedStructure = $iterator($pagesConfig['static-pages']);
+ $this->updateStructure($updatedStructure);
+ }
+
+ /**
+ * Returns the parsed meta/static-pages.yaml file contents.
+ * @return mixed
+ */
+ protected function getPagesConfig()
+ {
+ if (self::$configCache !== false) {
+ return self::$configCache;
+ }
+
+ $config = Meta::loadCached($this->theme, 'static-pages.yaml');
+
+ if (!$config) {
+ $config = new Meta();
+ $config->fileName = 'static-pages.yaml';
+ $config['static-pages'] = [];
+ $config->save();
+ }
+
+ if (!isset($config->attributes['static-pages'])) {
+ $config['static-pages'] = [];
+ }
+
+ return self::$configCache = $config;
+ }
+
+ /**
+ * Updates the page hierarchy structure in the theme's meta/static-pages.yaml file.
+ * @param array $structure A nested associative array representing the page structure
+ */
+ public function updateStructure($structure)
+ {
+ $config = $this->getPagesConfig();
+ $config['static-pages'] = $structure;
+ $config->save();
+ }
+}
diff --git a/plugins/rainlab/pages/classes/Router.php b/plugins/rainlab/pages/classes/Router.php
new file mode 100644
index 000000000..11e4d2c46
--- /dev/null
+++ b/plugins/rainlab/pages/classes/Router.php
@@ -0,0 +1,178 @@
+theme = $theme;
+ }
+
+ /**
+ * Finds a static page by its URL.
+ * @param string $url The requested URL string.
+ * @return \RainLab\Pages\Classes\Page Returns \RainLab\Pages\Classes\Page object or null if the page cannot be found.
+ */
+ public function findByUrl($url)
+ {
+ $url = Str::lower(RouterHelper::normalizeUrl($url));
+
+ if (array_key_exists($url, self::$cache)) {
+ return self::$cache[$url];
+ }
+
+ $urlMap = $this->getUrlMap();
+ $urlMap = array_key_exists('urls', $urlMap) ? $urlMap['urls'] : [];
+
+ if (!array_key_exists($url, $urlMap)) {
+ return null;
+ }
+
+ $fileName = $urlMap[$url];
+
+ if (($page = Page::loadCached($this->theme, $fileName)) === null) {
+ /*
+ * If the page was not found on the disk, clear the URL cache
+ * and try again.
+ */
+ $this->clearCache();
+
+ return self::$cache[$url] = Page::loadCached($this->theme, $fileName);
+ }
+
+ return self::$cache[$url] = $page;
+ }
+
+ /**
+ * Autoloads the URL map only allowing a single execution.
+ * @return array Returns the URL map.
+ */
+ protected function getUrlMap()
+ {
+ if (!count(self::$urlMap)) {
+ $this->loadUrlMap();
+ }
+
+ return self::$urlMap;
+ }
+
+ /**
+ * Loads the URL map - a list of page file names and corresponding URL patterns.
+ * The URL map can is cached. The clearUrlMap() method resets the cache. By default
+ * the map is updated every time when a page is saved in the back-end, or
+ * when the interval defined with the cms.urlCacheTtl expires.
+ * @return boolean Returns true if the URL map was loaded from the cache. Otherwise returns false.
+ */
+ protected function loadUrlMap()
+ {
+ $key = $this->getCacheKey('static-page-url-map');
+
+ $cacheable = Config::get('cms.enableRoutesCache', Config::get('cms.enable_route_cache', false));
+ $cached = $cacheable ? Cache::get($key, false) : false;
+
+ if (!$cached || ($unserialized = @unserialize($cached)) === false) {
+ /*
+ * The item doesn't exist in the cache, create the map
+ */
+ $pageList = new PageList($this->theme);
+
+ $pages = $pageList->listPages();
+ $map = [
+ 'urls' => [],
+ 'files' => [],
+ 'titles' => []
+ ];
+ foreach ($pages as $page) {
+ $url = $page->getViewBag()->property('url');
+ if (!$url) {
+ continue;
+ }
+
+ $url = Str::lower(RouterHelper::normalizeUrl($url));
+ $file = $page->getBaseFileName();
+
+ $map['urls'][$url] = $file;
+ $map['files'][$file] = $url;
+ $map['titles'][$file] = $page->getViewBag()->property('title');
+ }
+
+ self::$urlMap = $map;
+
+ if ($cacheable) {
+ $comboConfig = Config::get('cms.urlCacheTtl', Config::get('cms.url_cache_ttl', 10));
+ $expiresAt = now()->addMinutes($comboConfig);
+ Cache::put($key, serialize($map), $expiresAt);
+ }
+
+ return false;
+ }
+
+ self::$urlMap = $unserialized;
+
+ return true;
+ }
+
+ /**
+ * Returns the caching URL key depending on the theme.
+ * @param string $keyName Specifies the base key name.
+ * @return string Returns the theme-specific key name.
+ */
+ protected function getCacheKey($keyName)
+ {
+ $key = crc32($this->theme->getPath()).$keyName;
+ /**
+ * @event pages.router.getCacheKey
+ * Enables modifying the key used to reference cached RainLab.Pages routes
+ *
+ * Example usage:
+ *
+ * Event::listen('pages.router.getCacheKey', function (&$key) {
+ * $key = $key . '-' . App::getLocale();
+ * });
+ *
+ */
+ Event::fire('pages.router.getCacheKey', [&$key]);
+ return $key;
+ }
+
+ /**
+ * Clears the router cache.
+ */
+ public function clearCache()
+ {
+ Cache::forget($this->getCacheKey('static-page-url-map'));
+ }
+}
diff --git a/plugins/rainlab/pages/classes/Snippet.php b/plugins/rainlab/pages/classes/Snippet.php
new file mode 100644
index 000000000..9b89b0df9
--- /dev/null
+++ b/plugins/rainlab/pages/classes/Snippet.php
@@ -0,0 +1,687 @@
+getViewBag();
+
+ $this->code = $viewBag->property('snippetCode');
+ $this->description = $partial->description;
+ $this->name = $viewBag->property('snippetName');
+ $this->properties = $viewBag->property('snippetProperties', []);
+ }
+
+ /**
+ * Initializes the snippet from a CMS component information.
+ * @param string $componentClass Specifies the component class.
+ * @param string $componentCode Specifies the component code.
+ */
+ public function initFromComponentInfo($componentClass, $componentCode)
+ {
+ $this->code = $componentCode;
+ $this->componentClass = $componentClass;
+ }
+
+ /**
+ * Returns the snippet name.
+ * This method should not be used in the front-end request handling.
+ * @return string
+ */
+ public function getName()
+ {
+ if ($this->name !== null) {
+ return $this->name;
+ }
+
+ if ($this->componentClass === null) {
+ return null;
+ }
+
+ $component = $this->getComponent();
+
+ return $this->name = ComponentHelpers::getComponentName($component);
+ }
+
+ /**
+ * Returns the snippet description.
+ * This method should not be used in the front-end request handling.
+ * @return string
+ */
+ public function getDescription()
+ {
+ if ($this->description !== null) {
+ return $this->description;
+ }
+
+ if ($this->componentClass === null) {
+ return null;
+ }
+
+ $component = $this->getComponent();
+
+ return $this->description = ComponentHelpers::getComponentDescription($component);
+ }
+
+ /**
+ * Returns the snippet component class name.
+ * If the snippet is a partial snippet, returns NULL.
+ * @return string Returns the snippet component class name
+ */
+ public function getComponentClass()
+ {
+ return $this->componentClass;
+ }
+
+ /**
+ * Returns the snippet property list as array, in format compatible with Inspector.
+ */
+ public function getProperties()
+ {
+ if (!$this->componentClass) {
+ return self::parseIniProperties($this->properties);
+ }
+ else {
+ return ComponentHelpers::getComponentsPropertyConfig($this->getComponent(), false, true);
+ }
+ }
+
+ /**
+ * Returns a list of component definitions declared on the page.
+ * @param string $pageName Specifies the static page file name (the name of the corresponding content block file).
+ * @param \Cms\Classes\Theme $theme Specifies a parent theme.
+ * @return array Returns an array of component definitions
+ */
+ public static function listPageComponents($pageName, $theme, $markup)
+ {
+ $map = self::extractSnippetsFromMarkupCached($theme, $pageName, $markup);
+
+ $result = [];
+
+ foreach ($map as $snippetDeclaration => $snippetInfo) {
+ if (!isset($snippetInfo['component'])) {
+ continue;
+ }
+
+ $result[] = [
+ 'class' => $snippetInfo['component'],
+ 'alias' => $snippetInfo['code'],
+ 'properties' => $snippetInfo['properties']
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Extends the partial form with Snippet fields.
+ */
+ public static function extendPartialForm($formWidget)
+ {
+ /*
+ * Snippet code field
+ */
+
+ $fieldConfig = [
+ 'tab' => 'rainlab.pages::lang.snippet.partialtab',
+ 'type' => 'text',
+ 'label' => 'rainlab.pages::lang.snippet.code',
+ 'comment' => 'rainlab.pages::lang.snippet.code_comment',
+ 'span' => 'left'
+ ];
+
+ $formWidget->tabs['fields']['viewBag[snippetCode]'] = $fieldConfig;
+
+ /*
+ * Snippet description field
+ */
+
+ $fieldConfig = [
+ 'tab' => 'rainlab.pages::lang.snippet.partialtab',
+ 'type' => 'text',
+ 'label' => 'rainlab.pages::lang.snippet.name',
+ 'comment' => 'rainlab.pages::lang.snippet.name_comment',
+ 'span' => 'right'
+ ];
+
+ $formWidget->tabs['fields']['viewBag[snippetName]'] = $fieldConfig;
+
+ /*
+ * Snippet properties field
+ */
+
+ $fieldConfig = [
+ 'tab' => 'rainlab.pages::lang.snippet.partialtab',
+ 'type' => 'datatable',
+ 'height' => '150',
+ 'dynamicHeight' => true,
+ 'columns' => [
+ 'title' => [
+ 'title' => 'rainlab.pages::lang.snippet.column_property',
+ 'validation' => [
+ 'required' => [
+ 'message' => 'Please provide the property title',
+ 'requiredWith' => 'property'
+ ]
+ ]
+ ],
+ 'property' => [
+ 'title' => 'rainlab.pages::lang.snippet.column_code',
+ 'validation' => [
+ 'required' => [
+ 'message' => 'Please provide the property code',
+ 'requiredWith' => 'title'
+ ],
+ 'regex' => [
+ 'pattern' => '^[a-z][a-z0-9]*$',
+ 'modifiers' => 'i',
+ 'message' => Lang::get('rainlab.pages::lang.snippet.property_format_error')
+ ]
+ ]
+ ],
+ 'type' => [
+ 'title' => 'rainlab.pages::lang.snippet.column_type',
+ 'type' => 'dropdown',
+ 'options' => [
+ 'string' => 'rainlab.pages::lang.snippet.column_type_string',
+ 'checkbox' => 'rainlab.pages::lang.snippet.column_type_checkbox',
+ 'dropdown' => 'rainlab.pages::lang.snippet.column_type_dropdown'
+ ],
+ 'validation' => [
+ 'required' => [
+ 'requiredWith' => 'title'
+ ]
+ ]
+ ],
+ 'default' => [
+ 'title' => 'rainlab.pages::lang.snippet.column_default'
+ ],
+ 'options' => [
+ 'title' => 'rainlab.pages::lang.snippet.column_options'
+ ]
+ ]
+ ];
+
+ $formWidget->tabs['fields']['viewBag[snippetProperties]'] = $fieldConfig;
+ }
+
+ public static function extendEditorPartialToolbar($dataHolder)
+ {
+ $dataHolder->buttons[] = [
+ 'button' => 'rainlab.pages::lang.snippet.partialtab',
+ 'icon' => 'octo-icon-code-snippet',
+ 'popupTitle' => 'rainlab.pages::lang.snippet.settings_popup_title',
+ 'useViewBag' => true,
+ 'properties' => [
+ [
+ 'property' => 'snippetCode',
+ 'title' => 'rainlab.pages::lang.snippet.code',
+ 'description' => 'rainlab.pages::lang.snippet.code_comment',
+ 'type' => 'string',
+ 'validation' => [
+ 'required' => [
+ 'message' => 'rainlab.pages::lang.snippet.code_required'
+ ]
+ ]
+ ],
+ [
+ 'property' => 'snippetName',
+ 'title' => 'rainlab.pages::lang.snippet.name',
+ 'description' => 'rainlab.pages::lang.snippet.name_comment',
+ 'type' => 'string',
+ 'validation' => [
+ 'required' => [
+ 'message' => 'rainlab.pages::lang.snippet.name_required'
+ ]
+ ]
+ ],
+ [
+ 'property' => 'snippetProperties',
+ 'title' => '',
+ 'type' => 'table',
+ 'tab' => 'rainlab.pages::lang.snippet.properties',
+ 'columns' => [
+ [
+ 'column' => 'title',
+ 'type' => 'string',
+ 'title' => 'rainlab.pages::lang.snippet.column_property',
+ 'validation' => [
+ 'required' => [
+ 'message' => 'rainlab.pages::lang.snippet.title_required'
+ ]
+ ]
+ ],
+ [
+ 'column' => 'property',
+ 'type' => 'string',
+ 'title' => 'rainlab.pages::lang.snippet.column_code',
+ 'validation' => [
+ 'required' => [
+ 'message' => 'rainlab.pages::lang.snippet.property_required'
+ ],
+ 'regex' => [
+ 'pattern' => '^[a-z][a-z0-9]*$',
+ 'modifiers' => 'i',
+ 'message' => 'rainlab.pages::lang.snippet.property_format_error'
+ ]
+ ]
+ ],
+ [
+ 'column' => 'type',
+ 'title' => 'rainlab.pages::lang.snippet.column_type',
+ 'type' => 'dropdown',
+ 'placeholder' => 'rainlab.pages::lang.snippet.column_type_placeholder',
+ 'options' => [
+ 'string' => 'rainlab.pages::lang.snippet.column_type_string',
+ 'checkbox' => 'rainlab.pages::lang.snippet.column_type_checkbox',
+ 'dropdown' => 'rainlab.pages::lang.snippet.column_type_dropdown'
+ ],
+ 'validation' => [
+ 'required' => [
+ 'message' => 'rainlab.pages::lang.snippet.type_required'
+ ]
+ ]
+ ],
+ [
+ 'column' => 'default',
+ 'type' => 'string',
+ 'title' => 'rainlab.pages::lang.snippet.column_default'
+ ],
+ [
+ 'column' => 'options',
+ 'type' => 'string',
+ 'title' => 'rainlab.pages::lang.snippet.column_options'
+ ]
+ ]
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Returns a component corresponding to the snippet.
+ * This method should not be used in the front-end request handling code.
+ * @return \Cms\Classes\ComponentBase
+ */
+ protected function getComponent()
+ {
+ if ($this->componentClass === null) {
+ return null;
+ }
+
+ if ($this->componentObj !== null) {
+ return $this->componentObj;
+ }
+
+ $componentClass = $this->componentClass;
+
+ return $this->componentObj = new $componentClass();
+ }
+
+ //
+ // Parsing
+ //
+
+ /**
+ * Parses the static page markup and renders snippets defined on the page.
+ * @param string $pageName Specifies the static page file name (the name of the corresponding content block file).
+ * @param \Cms\Classes\Theme $theme Specifies a parent theme.
+ * @param string $markup Specifies the markup string to process.
+ * @return string Returns the processed string.
+ */
+ public static function processPageMarkup($pageName, $theme, $markup)
+ {
+ $map = self::extractSnippetsFromMarkupCached($theme, $pageName, $markup);
+
+ $controller = CmsController::getController();
+ $partialSnippetMap = SnippetManager::instance()->getPartialSnippetMap($theme);
+
+ foreach ($map as $snippetDeclaration => $snippetInfo) {
+ $snippetCode = $snippetInfo['code'];
+
+ if (!isset($snippetInfo['component'])) {
+ if (!array_key_exists($snippetCode, $partialSnippetMap)) {
+ throw new ApplicationException(sprintf('Partial for the snippet %s is not found', $snippetCode));
+ }
+
+ $partialName = $partialSnippetMap[$snippetCode];
+ $generatedMarkup = $controller->renderPartial($partialName, $snippetInfo['properties']);
+ }
+ else {
+ $generatedMarkup = $controller->renderComponent($snippetCode);
+ }
+
+ $pattern = preg_quote($snippetDeclaration);
+ $markup = mb_ereg_replace($pattern, $generatedMarkup, $markup);
+ }
+
+ return $markup;
+ }
+
+ public static function processTemplateSettingsArray($settingsArray)
+ {
+ if (
+ !isset($settingsArray['viewBag']['snippetProperties']['TableData']) &&
+ !isset($settingsArray['viewBag']['snippetProperties']) // CMS Editor
+ ) {
+ return $settingsArray;
+ }
+
+ $properties = [];
+
+ if (isset($settingsArray['viewBag']['snippetProperties']['TableData'])) {
+ $rows = $settingsArray['viewBag']['snippetProperties']['TableData'];
+ }
+ else {
+ $rows = $settingsArray['viewBag']['snippetProperties'];
+ }
+
+ foreach ($rows as $row) {
+ $property = array_get($row, 'property');
+ $settings = array_only($row, ['title', 'type', 'default', 'options']);
+
+ if (isset($settings['options'])) {
+ $settings['options'] = self::dropDownOptionsToArray($settings['options']);
+ }
+
+ $properties[$property] = $settings;
+ }
+
+ $settingsArray['viewBag']['snippetProperties'] = [];
+
+ foreach ($properties as $name => $value) {
+ $settingsArray['viewBag']['snippetProperties'][$name] = $value;
+ }
+
+ return $settingsArray;
+ }
+
+ public static function processTemplateSettings($template, $context = null)
+ {
+ if (!isset($template->viewBag['snippetProperties'])) {
+ return;
+ }
+
+ $parsedProperties = self::parseIniProperties($template->viewBag['snippetProperties']);
+
+ foreach ($parsedProperties as $index => &$property) {
+ if ($context !== 'editor') {
+ $property['id'] = $index;
+ }
+
+ if (isset($property['options']) && is_array($property['options'])) {
+ $property['options'] = self::dropDownOptionsToString($property['options']);
+ }
+ }
+
+ $template->viewBag['snippetProperties'] = $parsedProperties;
+
+ if ($context == 'editor') {
+ $template->settings['components']['viewBag'] = $template->viewBag;
+ }
+ }
+
+ /**
+ * Apples default property values and fixes property names.
+ *
+ * As snippet properties are defined with data attributes, they are lower case, whereas
+ * real property names are case sensitive. This method finds original property names defined
+ * in snippet classes or partials and replaces property names defined in the static page markup.
+ */
+ protected static function preprocessPropertyValues($theme, $snippetCode, $componentClass, $properties)
+ {
+ $snippet = SnippetManager::instance()->findByCodeOrComponent($theme, $snippetCode, $componentClass, true);
+ if (!$snippet) {
+ throw new ApplicationException(Lang::get('rainlab.pages::lang.snippet.not_found', ['code' => $snippetCode]));
+ }
+
+ $properties = array_change_key_case($properties);
+ $snippetProperties = $snippet->getProperties();
+
+ foreach ($snippetProperties as $propertyInfo) {
+ $propertyCode = $propertyInfo['property'];
+ $lowercaseCode = strtolower($propertyCode);
+
+ if (!array_key_exists($lowercaseCode, $properties)) {
+ if (array_key_exists('default', $propertyInfo)) {
+ $properties[$propertyCode] = $propertyInfo['default'];
+ }
+ }
+ else {
+ $markupPropertyInfo = $properties[$lowercaseCode];
+ unset($properties[$lowercaseCode]);
+ $properties[$propertyCode] = $markupPropertyInfo;
+ }
+ }
+
+ return $properties;
+ }
+
+ /**
+ * Converts a keyed object to an array, converting the index to the "property" value.
+ * @return array
+ */
+ protected static function parseIniProperties($properties)
+ {
+ foreach ($properties as $index => $value) {
+ $properties[$index]['property'] = $index;
+ }
+
+ return array_values($properties);
+ }
+
+ protected static function dropDownOptionsToArray($optionsString)
+ {
+ $options = explode('|', $optionsString);
+
+ $result = [];
+ foreach ($options as $index => $optionStr) {
+ $parts = explode(':', $optionStr, 2);
+
+ if (count($parts) > 1 ) {
+ $key = trim($parts[0]);
+
+ if (strlen($key)) {
+ if (!preg_match('/^[0-9a-z-_]+$/i', $key)) {
+ throw new ValidationException(['snippetProperties' => Lang::get('rainlab.pages::lang.snippet.invalid_option_key', ['key'=>$key])]);
+ }
+
+ $result[$key] = trim($parts[1]);
+ }
+ else {
+ $result[$index] = trim($optionStr);
+ }
+ }
+ else {
+ $result[$index] = trim($optionStr);
+ }
+ }
+
+ return $result;
+ }
+
+ protected static function dropDownOptionsToString($optionsArray)
+ {
+ $result = [];
+ $isAssoc = (bool) count(array_filter(array_keys($optionsArray), 'is_string'));
+
+ foreach ($optionsArray as $optionIndex => $optionValue) {
+ $result[] = $isAssoc
+ ? $optionIndex.':'.$optionValue
+ : $optionValue;
+ }
+
+ return implode(' | ', $result);
+ }
+
+ protected static function extractSnippetsFromMarkup($markup, $theme)
+ {
+ $map = [];
+ $matches = [];
+
+ if (preg_match_all('/\]+\>.*\<\/figure\>/i', $markup, $matches)) {
+ foreach ($matches[0] as $snippetDeclaration) {
+ $nameMatch = [];
+
+ if (!preg_match('/data\-snippet\s*=\s*"([^"]+)"/', $snippetDeclaration, $nameMatch)) {
+ continue;
+ }
+
+ $snippetCode = $nameMatch[1];
+
+ $properties = [];
+
+ $propertyMatches = [];
+ if (preg_match_all('/data\-property-(?[^=]+)\s*=\s*\"(?[^\"]+)\"/i', $snippetDeclaration, $propertyMatches)) {
+ foreach ($propertyMatches['property'] as $index => $propertyName) {
+ $properties[$propertyName] = $propertyMatches['value'][$index];
+ }
+ }
+
+ $componentMatch = [];
+ $componentClass = null;
+
+ if (preg_match('/data\-component\s*=\s*"([^"]+)"/', $snippetDeclaration, $componentMatch)) {
+ $componentClass = $componentMatch[1];
+ }
+
+ // Apply default values for properties not defined in the markup
+ // and normalize property code names.
+ $properties = self::preprocessPropertyValues($theme, $snippetCode, $componentClass, $properties);
+
+ $map[$snippetDeclaration] = [
+ 'code' => $snippetCode,
+ 'component' => $componentClass,
+ 'properties' => $properties
+ ];
+ }
+ }
+
+ return $map;
+ }
+
+ protected static function extractSnippetsFromMarkupCached($theme, $pageName, $markup)
+ {
+ if (array_key_exists($pageName, self::$pageSnippetMap)) {
+ return self::$pageSnippetMap[$pageName];
+ }
+
+ $key = self::getMapCacheKey($theme);
+
+ $map = null;
+ $cached = Cache::get($key, false);
+
+ if ($cached !== false && ($cached = @unserialize($cached)) !== false) {
+ if (array_key_exists($pageName, $cached)) {
+ $map = $cached[$pageName];
+ }
+ }
+
+ if (!is_array($map)) {
+ $map = self::extractSnippetsFromMarkup($markup, $theme);
+
+ if (!is_array($cached)) {
+ $cached = [];
+ }
+
+ $cached[$pageName] = $map;
+ $comboConfig = Config::get('cms.parsedPageCacheTTL', Config::get('cms.template_cache_ttl', 10));
+ $expiresAt = now()->addMinutes($comboConfig);
+ Cache::put($key, serialize($cached), $expiresAt);
+ }
+
+ self::$pageSnippetMap[$pageName] = $map;
+
+ return $map;
+ }
+
+ /**
+ * Returns a cache key for this record.
+ */
+ protected static function getMapCacheKey($theme)
+ {
+ $key = crc32($theme->getPath()).'snippet-map';
+ /**
+ * @event pages.snippet.getMapCacheKey
+ * Enables modifying the key used to reference cached RainLab.Pages snippet maps
+ *
+ * Example usage:
+ *
+ * Event::listen('pages.snippet.getMapCacheKey', function (&$key) {
+ * $key = $key . '-' . App::getLocale();
+ * });
+ *
+ */
+ Event::fire('pages.snippet.getMapCacheKey', [&$key]);
+ return $key;
+ }
+
+ /**
+ * Clears the snippet map item cache
+ * @param \Cms\Classes\Theme $theme Specifies the current theme.
+ */
+ public static function clearMapCache($theme)
+ {
+ Cache::forget(self::getMapCacheKey($theme));
+ }
+}
diff --git a/plugins/rainlab/pages/classes/SnippetManager.php b/plugins/rainlab/pages/classes/SnippetManager.php
new file mode 100644
index 000000000..1d9af0b2c
--- /dev/null
+++ b/plugins/rainlab/pages/classes/SnippetManager.php
@@ -0,0 +1,278 @@
+snippets !== null) {
+ return $this->snippets;
+ }
+
+ $themeSnippets = $this->listThemeSnippets($theme);
+ $componentSnippets = $this->listComponentSnippets();
+
+ $this->snippets = array_merge($themeSnippets, $componentSnippets);
+
+ /*
+ * @event pages.snippets.listSnippets
+ * Gives the ability to manage the snippet list dynamically.
+ *
+ * Example usage to add a snippet to the list:
+ *
+ * Event::listen('pages.snippets.listSnippets', function($manager) {
+ * $snippet = new \RainLab\Pages\Classes\Snippet();
+ * $snippet->initFromComponentInfo('\Example\Plugin\Components\ComponentClass', 'snippetCode');
+ * $manager->addSnippet($snippet);
+ * });
+ *
+ * Example usage to remove a snippet from the list:
+ *
+ * Event::listen('pages.snippets.listSnippets', function($manager) {
+ * $manager->removeSnippet('snippetCode');
+ * });
+ */
+ Event::fire('pages.snippets.listSnippets', [$this]);
+
+ return $this->snippets;
+ }
+
+ /**
+ * Add snippet to the list of snippets
+ *
+ * @param Snippet $snippet
+ * @return void
+ */
+ public function addSnippet(Snippet $snippet)
+ {
+ $this->snippets[] = $snippet;
+ }
+
+ /**
+ * Remove a snippet with the given code from the list of snippets
+ *
+ * @param string $snippetCode
+ * @return void
+ */
+ public function removeSnippet(string $snippetCode)
+ {
+ $this->snippets = array_filter($this->snippets, function ($snippet) use ($snippetCode) {
+ return $snippet->code !== $snippetCode;
+ });
+ }
+
+ /**
+ * Finds a snippet by its code.
+ * This method is used internally by the system.
+ * @param \Cms\Classes\Theme $theme Specifies a parent theme.
+ * @param string $code Specifies the snippet code.
+ * @param string $$componentClass Specifies the snippet component class, if available.
+ * @param boolean $allowCaching Specifies whether caching is allowed for the call.
+ * @return array Returns an array of Snippet objects.
+ */
+ public function findByCodeOrComponent($theme, $code, $componentClass, $allowCaching = false)
+ {
+ if (!$allowCaching) {
+ // If caching is not allowed, list all available snippets,
+ // find the snippet in the list and return it.
+ $snippets = $this->listSnippets($theme);
+
+ foreach ($snippets as $snippet) {
+ if ($componentClass && $snippet->getComponentClass() == $componentClass) {
+ return $snippet;
+ }
+
+ if ($snippet->code == $code) {
+ return $snippet;
+ }
+ }
+
+ return null;
+ }
+
+ // If caching is allowed, and the requested snippet is a partial snippet,
+ // try to load the partial name from the cache and initialize the snippet
+ // from the partial.
+
+ if (!strlen($componentClass)) {
+ $map = $this->getPartialSnippetMap($theme);
+
+ if (!array_key_exists($code, $map)) {
+ return null;
+ }
+
+ $partialName = $map[$code];
+ $partial = Partial::loadCached($theme, $partialName);
+
+ if (!$partial) {
+ return null;
+ }
+
+ $snippet = new Snippet;
+ $snippet->initFromPartial($partial);
+
+ return $snippet;
+ }
+ else {
+ // If the snippet is a component snippet, initialize it
+ // from the component
+
+ if (!class_exists($componentClass)) {
+ throw new SystemException(sprintf('The snippet component class %s is not found.', $componentClass));
+ }
+
+ $snippet = new Snippet;
+ $snippet->initFromComponentInfo($componentClass, $code);
+
+ return $snippet;
+ }
+ }
+
+ /**
+ * Clears front-end run-time cache.
+ * @param \Cms\Classes\Theme $theme Specifies a parent theme.
+ */
+ public static function clearCache($theme)
+ {
+ Cache::forget(self::getPartialMapCacheKey($theme));
+
+ Snippet::clearMapCache($theme);
+ }
+
+ /**
+ * Returns a cache key for this record.
+ */
+ protected static function getPartialMapCacheKey($theme)
+ {
+ $key = crc32($theme->getPath()).'snippet-partial-map';
+ /**
+ * @event pages.snippet.getPartialMapCacheKey
+ * Enables modifying the key used to reference cached RainLab.Pages partial maps
+ *
+ * Example usage:
+ *
+ * Event::listen('pages.snippet.getPartialMapCacheKey', function (&$key) {
+ * $key = $key . '-' . App::getLocale();
+ * });
+ *
+ */
+ Event::fire('pages.snippet.getPartialMapCacheKey', [&$key]);
+ return $key;
+ }
+
+ /**
+ * Returns a list of partial-based snippets and corresponding partial names.
+ * @param \Cms\Classes\Theme $theme Specifies a parent theme.
+ * @return Returns an associative array with the snippet code in keys and partial file names in values.
+ */
+ public function getPartialSnippetMap($theme)
+ {
+ $key = self::getPartialMapCacheKey($theme);
+
+ $result = [];
+ $cached = Cache::get($key, false);
+
+ if ($cached !== false && ($cached = @unserialize($cached)) !== false) {
+ return $cached;
+ }
+
+ $partials = Partial::listInTheme($theme);
+
+ foreach ($partials as $partial) {
+ $viewBag = $partial->getViewBag();
+
+ $snippetCode = $viewBag->property('snippetCode');
+ if (!strlen($snippetCode)) {
+ continue;
+ }
+
+ $result[$snippetCode] = $partial->getFileName();
+ }
+
+ $comboConfig = Config::get('cms.parsedPageCacheTTL', Config::get('cms.template_cache_ttl', 10));
+ $expiresAt = now()->addMinutes($comboConfig);
+ Cache::put($key, serialize($result), $expiresAt);
+
+ return $result;
+ }
+
+ /**
+ * Returns a list of snippets in the specified theme.
+ * @param \Cms\Classes\Theme $theme Specifies a parent theme.
+ * @return array Returns an array of Snippet objects.
+ */
+ protected function listThemeSnippets($theme)
+ {
+ $result = [];
+
+ $partials = Partial::listInTheme($theme, true);
+
+ foreach ($partials as $partial) {
+ $viewBag = $partial->getViewBag();
+
+ if (strlen($viewBag->property('snippetCode'))) {
+ $snippet = new Snippet;
+ $snippet->initFromPartial($partial);
+ $result[] = $snippet;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a list of snippets created from components.
+ * @return array Returns an array of Snippet objects.
+ */
+ protected function listComponentSnippets()
+ {
+ $result = [];
+
+ $pluginManager = PluginManager::instance();
+ $plugins = $pluginManager->getPlugins();
+
+ foreach ($plugins as $id => $plugin) {
+ if (!method_exists($plugin, 'registerPageSnippets')) {
+ continue;
+ }
+
+ $snippets = $plugin->registerPageSnippets();
+ if (!is_array($snippets)) {
+ continue;
+ }
+
+ foreach ($snippets as $componentClass => $componentCode) {
+ // TODO: register snippet components later, during
+ // the page life cycle.
+ $snippet = new Snippet;
+ $snippet->initFromComponentInfo($componentClass, $componentCode);
+ $result[] = $snippet;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/rainlab/pages/classes/content/fields.yaml b/plugins/rainlab/pages/classes/content/fields.yaml
new file mode 100644
index 000000000..57a5de973
--- /dev/null
+++ b/plugins/rainlab/pages/classes/content/fields.yaml
@@ -0,0 +1,38 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ fileName:
+ label: cms::lang.editor.filename
+ attributes:
+ default-focus: 1
+
+ toolbar:
+ type: partial
+ path: content_toolbar
+ cssClass: collapse-visible
+
+ components: Cms\FormWidgets\Components
+
+secondaryTabs:
+ stretch: true
+ fields:
+ markup:
+ tab: cms::lang.editor.content
+ stretch: true
+ type: codeeditor
+ language: html
+ theme: chrome
+ showGutter: false
+ highlightActiveLine: false
+ fontSize: 13
+ cssClass: pagesTextEditor
+ margin: 20
+
+ markup_html:
+ tab: cms::lang.editor.content
+ stretch: true
+ type: richeditor
+ size: huge
+ valueFrom: markup
\ No newline at end of file
diff --git a/plugins/rainlab/pages/classes/menu/fields.yaml b/plugins/rainlab/pages/classes/menu/fields.yaml
new file mode 100644
index 000000000..07a139152
--- /dev/null
+++ b/plugins/rainlab/pages/classes/menu/fields.yaml
@@ -0,0 +1,33 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ name:
+ span: left
+ label: rainlab.pages::lang.menu.name
+ placeholder: rainlab.pages::lang.menu.new_name
+ attributes:
+ default-focus: 1
+
+ code:
+ span: right
+ placeholder: rainlab.pages::lang.menu.new_code
+ label: rainlab.pages::lang.menu.code
+ preset:
+ field: name
+ type: file
+
+ toolbar:
+ type: partial
+ path: menu_toolbar
+ cssClass: collapse-visible
+
+tabs:
+ stretch: true
+ cssClass: master-area
+ fields:
+ items:
+ stretch: true
+ tab: rainlab.pages::lang.menu.items
+ type: RainLab\Pages\FormWidgets\MenuItems
diff --git a/plugins/rainlab/pages/classes/menuitem/fields.yaml b/plugins/rainlab/pages/classes/menuitem/fields.yaml
new file mode 100644
index 000000000..e4fa52850
--- /dev/null
+++ b/plugins/rainlab/pages/classes/menuitem/fields.yaml
@@ -0,0 +1,69 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+
+ search:
+ type: Rainlab\Pages\FormWidgets\MenuItemSearch
+
+ title:
+ span: left
+ label: rainlab.pages::lang.menuitem.title
+
+ type:
+ span: right
+ label: rainlab.pages::lang.menuitem.type
+ type: dropdown
+
+ url:
+ label: rainlab.pages::lang.menuitem.url
+
+ reference:
+ label: rainlab.pages::lang.menuitem.reference
+ type: dropdown
+ cssClass: input-sidebar-control
+
+ cmsPage:
+ label: rainlab.pages::lang.menuitem.cms_page
+ comment: rainlab.pages::lang.menuitem.cms_page_comment
+ type: dropdown
+ cssClass: input-sidebar-control
+
+ nesting:
+ label: rainlab.pages::lang.menuitem.allow_nested_items
+ comment: rainlab.pages::lang.menuitem.allow_nested_items_comment
+ type: checkbox
+ default: true
+
+ replace:
+ label: rainlab.pages::lang.menuitem.replace
+ comment: rainlab.pages::lang.menuitem.replace_comment
+ type: checkbox
+ default: true
+
+tabs:
+ fields:
+ viewBag[isHidden]:
+ label: rainlab.pages::lang.menuitem.hidden
+ comment: rainlab.pages::lang.menuitem.hidden_comment
+ type: checkbox
+ tab: rainlab.pages::lang.menuitem.display_tab
+
+ code:
+ label: rainlab.pages::lang.menuitem.code
+ comment: rainlab.pages::lang.menuitem.code_comment
+ tab: rainlab.pages::lang.menuitem.attributes_tab
+ span: auto
+
+ viewBag[cssClass]:
+ label: rainlab.pages::lang.menuitem.css_class
+ comment: rainlab.pages::lang.menuitem.css_class_comment
+ tab: rainlab.pages::lang.menuitem.attributes_tab
+ span: auto
+
+ viewBag[isExternal]:
+ label: rainlab.pages::lang.menuitem.external_link
+ comment: rainlab.pages::lang.menuitem.external_link_comment
+ type: checkbox
+ tab: rainlab.pages::lang.menuitem.attributes_tab
diff --git a/plugins/rainlab/pages/classes/page/fields.yaml b/plugins/rainlab/pages/classes/page/fields.yaml
new file mode 100644
index 000000000..9a2350e90
--- /dev/null
+++ b/plugins/rainlab/pages/classes/page/fields.yaml
@@ -0,0 +1,68 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ viewBag[title]:
+ span: left
+ label: rainlab.pages::lang.editor.title
+ placeholder: rainlab.pages::lang.editor.new_title
+ attributes:
+ default-focus: 1
+
+ viewBag[url]:
+ span: right
+ placeholder: /
+ label: rainlab.pages::lang.editor.url
+ preset:
+ field: viewBag[title]
+ type: url
+ prefixInput: input[data-parent-url]
+
+ toolbar:
+ type: partial
+ path: page_toolbar
+ cssClass: collapse-visible
+
+tabs:
+ cssClass: master-area
+ fields:
+ viewBag[layout]:
+ tab: cms::lang.editor.settings
+ label: rainlab.pages::lang.page.layout
+ type: dropdown
+ options: getLayoutOptions
+
+ viewBag[is_hidden]:
+ tab: cms::lang.editor.settings
+ span: left
+ label: rainlab.pages::lang.editor.hidden
+ type: checkbox
+ comment: rainlab.pages::lang.editor.hidden_comment
+
+ viewBag[navigation_hidden]:
+ tab: cms::lang.editor.settings
+ span: right
+ label: rainlab.pages::lang.editor.navigation_hidden
+ type: checkbox
+ comment: rainlab.pages::lang.editor.navigation_hidden_comment
+
+ viewBag[meta_title]:
+ tab: cms::lang.editor.meta
+ label: cms::lang.editor.meta_title
+
+ viewBag[meta_description]:
+ tab: cms::lang.editor.meta
+ label: cms::lang.editor.meta_description
+ type: textarea
+ size: tiny
+
+secondaryTabs:
+ stretch: true
+ fields:
+ markup:
+ tab: rainlab.pages::lang.editor.content
+ type: richeditor
+ legacyMode: true
+ stretch: true
+ size: huge
diff --git a/plugins/rainlab/pages/components/ChildPages.php b/plugins/rainlab/pages/components/ChildPages.php
new file mode 100644
index 000000000..7efba1135
--- /dev/null
+++ b/plugins/rainlab/pages/components/ChildPages.php
@@ -0,0 +1,60 @@
+ '',
+ * 'title' => '',
+ * 'page' => \RainLab\Pages\Classes\Page,
+ * 'viewBag' => array,
+ * 'is_hidden' => bool,
+ * 'navigation_hidden' => bool,
+ * ]
+ */
+ public $pages = [];
+
+ public function componentDetails()
+ {
+ return [
+ 'name' => 'rainlab.pages::lang.component.child_pages_name',
+ 'description' => 'rainlab.pages::lang.component.child_pages_description'
+ ];
+ }
+
+ public function onRun()
+ {
+ // Check if the staticPage component is attached to the rendering template
+ $this->staticPageComponent = $this->findComponentByName('staticPage');
+ if ($this->staticPageComponent->pageObject) {
+ $this->childPages = $this->staticPageComponent->pageObject->getChildren();
+
+ if ($this->childPages) {
+ foreach ($this->childPages as $childPage) {
+ $viewBag = $childPage->viewBag;
+ $this->pages = array_merge($this->pages, [[
+ 'url' => @$viewBag['url'],
+ 'title' => @$viewBag['title'],
+ 'page' => $childPage,
+ 'viewBag' => $viewBag,
+ 'is_hidden' => @$viewBag['is_hidden'],
+ 'navigation_hidden' => @$viewBag['navigation_hidden'],
+ ]]);
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/rainlab/pages/components/StaticBreadcrumbs.php b/plugins/rainlab/pages/components/StaticBreadcrumbs.php
new file mode 100644
index 000000000..a1aa82208
--- /dev/null
+++ b/plugins/rainlab/pages/components/StaticBreadcrumbs.php
@@ -0,0 +1,77 @@
+ 'rainlab.pages::lang.component.static_breadcrumbs_name',
+ 'description' => 'rainlab.pages::lang.component.static_breadcrumbs_description'
+ ];
+ }
+
+ public function onRun()
+ {
+ $url = $this->getRouter()->getUrl();
+
+ if (!strlen($url)) {
+ $url = '/';
+ }
+
+ $theme = Theme::getActiveTheme();
+ $router = new Router($theme);
+ $page = $router->findByUrl($url);
+
+ if ($page) {
+ $tree = StaticPageClass::buildMenuTree($theme);
+
+ $code = $startCode = $page->getBaseFileName();
+ $breadcrumbs = [];
+
+ while ($code) {
+ if (!isset($tree[$code])) {
+ break;
+ }
+
+ $pageInfo = $tree[$code];
+
+ if ($pageInfo['navigation_hidden']) {
+ $code = $pageInfo['parent'];
+ continue;
+ }
+
+ $reference = new MenuItemReference();
+ $reference->title = $pageInfo['title'];
+ $reference->url = StaticPageClass::url($code);
+ $reference->isActive = $code == $startCode;
+
+ $breadcrumbs[] = $reference;
+
+ $code = $pageInfo['parent'];
+ }
+
+ $breadcrumbs = array_reverse($breadcrumbs);
+
+ $this->breadcrumbs = $this->page['breadcrumbs'] = $breadcrumbs;
+ }
+ }
+}
diff --git a/plugins/rainlab/pages/components/StaticMenu.php b/plugins/rainlab/pages/components/StaticMenu.php
new file mode 100644
index 000000000..77d3aff76
--- /dev/null
+++ b/plugins/rainlab/pages/components/StaticMenu.php
@@ -0,0 +1,121 @@
+ 'rainlab.pages::lang.component.static_menu_name',
+ 'description' => 'rainlab.pages::lang.component.static_menu_description'
+ ];
+ }
+
+ public function defineProperties()
+ {
+ return [
+ 'code' => [
+ 'title' => 'rainlab.pages::lang.component.static_menu_code_name',
+ 'description' => 'rainlab.pages::lang.component.static_menu_code_description',
+ 'type' => 'dropdown'
+ ]
+ ];
+ }
+
+ public function getCodeOptions()
+ {
+ $result = [];
+
+ $theme = Theme::getEditTheme();
+ $menus = PagesMenu::listInTheme($theme, true);
+
+ foreach ($menus as $menu) {
+ $result[$menu->code] = $menu->name;
+ }
+
+ return $result;
+ }
+
+ public function onRun()
+ {
+ $this->page['menuItems'] = $this->menuItems();
+ }
+
+ public function menuItems()
+ {
+ if ($this->menuItems !== null) {
+ return $this->menuItems;
+ }
+
+ if (!strlen($this->property('code'))) {
+ return;
+ }
+
+ $theme = Theme::getActiveTheme();
+ $menu = PagesMenu::loadCached($theme, $this->property('code'));
+
+ if ($menu) {
+ $this->menuItems = $menu->generateReferences($this->page);
+ $this->menuName = $menu->name;
+ }
+
+ return $this->menuItems;
+ }
+
+ /**
+ * Counts the total menu items, including children.
+ */
+ public function totalItems()
+ {
+ $countAll = function($items) use (&$countAll) {
+ $count = count($items);
+
+ foreach ($items as $item) {
+ if (!isset($item->items)) {
+ continue;
+ }
+
+ $count += $countAll($item->items);
+ }
+
+ return $count;
+ };
+
+ return $countAll($this->menuItems());
+ }
+
+ /**
+ * Resets the menu code and rebuilds the menu.
+ * @param string $code
+ * @return array
+ */
+ public function resetMenu($code)
+ {
+ $this->setProperty('code', $code);
+ $this->menuItems = null;
+
+ return $this->page['menuItems'] = $this->menuItems();
+ }
+}
diff --git a/plugins/rainlab/pages/components/StaticPage.php b/plugins/rainlab/pages/components/StaticPage.php
new file mode 100644
index 000000000..d05f4019c
--- /dev/null
+++ b/plugins/rainlab/pages/components/StaticPage.php
@@ -0,0 +1,176 @@
+ 'rainlab.pages::lang.component.static_page_name',
+ 'description' => 'rainlab.pages::lang.component.static_page_description'
+ ];
+ }
+
+ public function defineProperties()
+ {
+ return [
+ 'useContent' => [
+ 'title' => 'rainlab.pages::lang.component.static_page_use_content_name',
+ 'description' => 'rainlab.pages::lang.component.static_page_use_content_description',
+ 'default' => 1,
+ 'type' => 'checkbox',
+ 'showExternalParam' => false
+ ],
+ 'default' => [
+ 'title' => 'rainlab.pages::lang.component.static_page_default_name',
+ 'description' => 'rainlab.pages::lang.component.static_page_default_description',
+ 'default' => 0,
+ 'type' => 'checkbox',
+ 'showExternalParam' => false
+ ],
+ 'childLayout' => [
+ 'title' => 'rainlab.pages::lang.component.static_page_child_layout_name',
+ 'description' => 'rainlab.pages::lang.component.static_page_child_layout_description',
+ 'type' => 'string',
+ 'showExternalParam' => false
+ ]
+
+ ];
+ }
+
+ public function onRun()
+ {
+ $url = $this->getRouter()->getUrl();
+
+ if (!strlen($url)) {
+ $url = '/';
+ }
+
+ $router = new Router(Theme::getActiveTheme());
+ $this->pageObject = $this->page['page'] = $router->findByUrl($url);
+
+ if ($this->pageObject) {
+ $this->title = $this->page['title'] = array_get($this->pageObject->viewBag, 'title');
+ $this->extraData = $this->page['extraData'] = $this->defineExtraData();
+ }
+ }
+
+ public function page()
+ {
+ return $this->pageObject;
+ }
+
+ public function parent()
+ {
+ return $this->pageObject ? $this->pageObject->getParent() : null;
+ }
+
+ public function children()
+ {
+ return $this->pageObject ? $this->pageObject->getChildren() : null;
+ }
+
+ public function content()
+ {
+ // Evaluate the content property only when it's requested in the
+ // render time. Calling the page's getProcessedMarkup() method in the
+ // onRun() handler is too early as it triggers rendering component-based
+ // snippets defined on the static page too early in the page life cycle. -ab
+
+ if ($this->contentCached !== false) {
+ return $this->contentCached;
+ }
+
+ if ($this->pageObject) {
+ return $this->contentCached = $this->pageObject->getProcessedMarkup();
+ }
+
+ $this->contentCached = '';
+ }
+
+ /**
+ * Find foreign view bag values and add them to
+ * the component and page vars.
+ */
+ protected function defineExtraData()
+ {
+ $standardProperties = [
+ 'title',
+ 'url',
+ 'layout',
+ 'is_hidden',
+ 'navigation_hidden',
+ 'meta_title',
+ 'meta_description'
+ ];
+
+ $extraData = array_diff_key(
+ $this->pageObject->viewBag,
+ array_flip($standardProperties)
+ );
+
+ foreach ($extraData as $key => $value) {
+ $this->page[$key] = $value;
+ }
+
+ return $extraData;
+ }
+
+ /**
+ * Implements the getter functionality.
+ * @param string $name
+ * @return void
+ */
+ public function __get($name)
+ {
+ if (array_key_exists($name, $this->extraData)) {
+ return $this->extraData[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Determine if an attribute exists on the object.
+ * @param string $key
+ * @return void
+ */
+ public function __isset($key)
+ {
+ if (array_key_exists($key, $this->extraData)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/rainlab/pages/components/childpages/default.htm b/plugins/rainlab/pages/components/childpages/default.htm
new file mode 100644
index 000000000..b2b402ce0
--- /dev/null
+++ b/plugins/rainlab/pages/components/childpages/default.htm
@@ -0,0 +1,9 @@
+{% if __SELF__.pages is not empty %}
+
+{% endif %}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/components/staticbreadcrumbs/default.htm b/plugins/rainlab/pages/components/staticbreadcrumbs/default.htm
new file mode 100644
index 000000000..1e886a51d
--- /dev/null
+++ b/plugins/rainlab/pages/components/staticbreadcrumbs/default.htm
@@ -0,0 +1,9 @@
+{% if breadcrumbs %}
+
+{% endif %}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/components/staticmenu/default.htm b/plugins/rainlab/pages/components/staticmenu/default.htm
new file mode 100644
index 000000000..a974a7e9b
--- /dev/null
+++ b/plugins/rainlab/pages/components/staticmenu/default.htm
@@ -0,0 +1,5 @@
+{% if __SELF__.menuItems %}
+
+ {% partial __SELF__ ~ "::items" items=__SELF__.menuItems %}
+
+{% endif %}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/components/staticmenu/items.htm b/plugins/rainlab/pages/components/staticmenu/items.htm
new file mode 100644
index 000000000..dea010750
--- /dev/null
+++ b/plugins/rainlab/pages/components/staticmenu/items.htm
@@ -0,0 +1,15 @@
+{% for item in items if not item.viewBag.isHidden %}
+
+ {% if item.url %}
+
+ {{ item.title }}
+
+ {% else %}
+ {{ item.title }}
+ {% endif %}
+
+ {% if item.items %}
+ {% partial __SELF__ ~ "::items" items=item.items %}
+ {% endif %}
+
+{% endfor %}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/components/staticpage/default.htm b/plugins/rainlab/pages/components/staticpage/default.htm
new file mode 100644
index 000000000..53cfbbc5a
--- /dev/null
+++ b/plugins/rainlab/pages/components/staticpage/default.htm
@@ -0,0 +1 @@
+{{ __SELF__.content|raw }}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/composer.json b/plugins/rainlab/pages/composer.json
new file mode 100644
index 000000000..a7fc74d22
--- /dev/null
+++ b/plugins/rainlab/pages/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "rainlab/pages-plugin",
+ "type": "october-plugin",
+ "description": "Pages plugin for October CMS",
+ "homepage": "https://octobercms.com/plugin/rainlab-pages",
+ "keywords": ["october", "octobercms", "pages"],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Alexey Bobkov",
+ "email": "aleksey.bobkov@gmail.com",
+ "role": "Co-founder"
+ },
+ {
+ "name": "Samuel Georges",
+ "email": "daftspunky@gmail.com",
+ "role": "Co-founder"
+ }
+ ],
+ "require": {
+ "php": ">=5.5.9",
+ "composer/installers": "~1.0"
+ },
+ "minimum-stability": "dev"
+}
diff --git a/plugins/rainlab/pages/controllers/Index.php b/plugins/rainlab/pages/controllers/Index.php
new file mode 100644
index 000000000..5c185fc5a
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/Index.php
@@ -0,0 +1,939 @@
+theme = Theme::getEditTheme())) {
+ throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_found'));
+ }
+
+ if ($this->user) {
+ if ($this->user->hasAccess('rainlab.pages.manage_pages')) {
+ new PageList($this, 'pageList');
+ $this->vars['activeWidgets'][] = 'pageList';
+ }
+
+ if ($this->user->hasAccess('rainlab.pages.manage_menus')) {
+ new MenuList($this, 'menuList');
+ $this->vars['activeWidgets'][] = 'menuList';
+ }
+
+ if ($this->user->hasAccess('rainlab.pages.manage_content')) {
+ new TemplateList($this, 'contentList', function() {
+ return $this->getContentTemplateList();
+ });
+ $this->vars['activeWidgets'][] = 'contentList';
+ }
+
+ if ($this->user->hasAccess('rainlab.pages.access_snippets')) {
+ new SnippetList($this, 'snippetList');
+ $this->vars['activeWidgets'][] = 'snippetList';
+ }
+ }
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ $context = [
+ 'pageList' => 'pages',
+ 'menuList' => 'menus',
+ 'contentList' => 'content',
+ 'snippetList' => 'snippets',
+ ];
+
+ BackendMenu::setContext('RainLab.Pages', 'pages', @$context[$this->vars['activeWidgets'][0]]);
+ }
+
+ //
+ // Pages, menus and text blocks
+ //
+
+ public function index()
+ {
+ $this->addJs('/modules/backend/assets/js/october.treeview.js', 'core');
+ $this->addJs('/plugins/rainlab/pages/assets/js/pages-page.js', 'RainLab.Pages');
+ $this->addJs('/plugins/rainlab/pages/assets/js/pages-snippets.js', 'RainLab.Pages');
+ $this->addCss('/plugins/rainlab/pages/assets/css/pages.css', 'RainLab.Pages');
+
+ // Preload the code editor class as it could be needed
+ // before it loads dynamically.
+ $this->addJs('/modules/backend/formwidgets/codeeditor/assets/js/build-min.js', 'core');
+
+ $this->bodyClass = 'compact-container';
+ $this->pageTitle = 'rainlab.pages::lang.plugin.name';
+ $this->pageTitleTemplate = Lang::get('rainlab.pages::lang.page.template_title');
+
+ if (Request::ajax() && Request::input('formWidgetAlias')) {
+ $this->bindFormWidgetToController();
+ }
+
+ $this->vars['layoutIgnoreTouchNavigation'] = true;
+ }
+
+ public function index_onOpen()
+ {
+ $this->validateRequestTheme();
+
+ $type = Request::input('type');
+ $object = $this->loadObject($type, Request::input('path'));
+
+ return $this->pushObjectForm($type, $object);
+ }
+
+ public function onSave()
+ {
+ $this->validateRequestTheme();
+ $type = Request::input('objectType');
+
+ $object = $this->fillObjectFromPost($type);
+ $object->save();
+
+ /*
+ * Extensibility
+ */
+ Event::fire('pages.object.save', [$this, $object, $type]);
+ $this->fireEvent('object.save', [$object, $type]);
+
+ $result = $this->getUpdateResponse($object, $type);
+
+ $successMessages = [
+ 'page' => 'rainlab.pages::lang.page.saved',
+ 'menu' => 'rainlab.pages::lang.menu.saved',
+ 'content' => 'rainlab.pages::lang.content.saved',
+ ];
+
+ $successMessage = isset($successMessages[$type])
+ ? $successMessages[$type]
+ : $successMessages['page'];
+
+ Flash::success(Lang::get($successMessage));
+
+ return $result;
+ }
+
+ public function onCreateObject()
+ {
+ $this->validateRequestTheme();
+
+ $type = Request::input('type');
+ $object = $this->createObject($type);
+ $parent = Request::input('parent');
+ $parentPage = null;
+
+ if ($type == 'page') {
+ if (strlen($parent)) {
+ $parentPage = StaticPage::load($this->theme, $parent);
+ }
+
+ $object->setDefaultLayout($parentPage);
+ }
+
+ $widget = $this->makeObjectFormWidget($type, $object);
+ $this->vars['objectPath'] = '';
+ $this->vars['canCommit'] = $this->canCommitObject($object);
+ $this->vars['canReset'] = $this->canResetObject($object);
+
+ $result = [
+ 'tabTitle' => $this->getTabTitle($type, $object),
+ 'tab' => $this->makePartial('form_page', [
+ 'form' => $widget,
+ 'objectType' => $type,
+ 'objectTheme' => $this->theme->getDirName(),
+ 'objectMtime' => null,
+ 'objectParent' => $parent,
+ 'parentPage' => $parentPage
+ ])
+ ];
+
+ return $result;
+ }
+
+ public function onDelete()
+ {
+ $this->validateRequestTheme();
+
+ $type = Request::input('objectType');
+
+ $deletedObjects = $this->loadObject($type, trim(Request::input('objectPath')))->delete();
+
+ $result = [
+ 'deletedObjects' => $deletedObjects,
+ 'theme' => $this->theme->getDirName()
+ ];
+
+ return $result;
+ }
+
+ public function onDeleteObjects()
+ {
+ $this->validateRequestTheme();
+
+ $type = Request::input('type');
+ $objects = Request::input('object');
+
+ if (!$objects) {
+ $objects = Request::input('template');
+ }
+
+ $error = null;
+ $deleted = [];
+
+ try {
+ foreach ($objects as $path => $selected) {
+ if (!$selected) {
+ continue;
+ }
+ $object = $this->loadObject($type, $path, true);
+ if (!$object) {
+ continue;
+ }
+
+ $deletedObjects = $object->delete();
+ if (is_array($deletedObjects)) {
+ $deleted = array_merge($deleted, $deletedObjects);
+ }
+ else {
+ $deleted[] = $path;
+ }
+ }
+ }
+ catch (Exception $ex) {
+ $error = $ex->getMessage();
+ }
+
+ return [
+ 'deleted' => $deleted,
+ 'error' => $error,
+ 'theme' => Request::input('theme')
+ ];
+ }
+
+ public function onOpenConcurrencyResolveForm()
+ {
+ return $this->makePartial('concurrency_resolve_form');
+ }
+
+ public function onGetMenuItemTypeInfo()
+ {
+ $type = Request::input('type');
+
+ return [
+ 'menuItemTypeInfo' => MenuItem::getTypeInfo($type)
+ ];
+ }
+
+ public function onUpdatePageLayout()
+ {
+ $this->validateRequestTheme();
+
+ $type = Request::input('objectType');
+
+ $object = $this->fillObjectFromPost($type);
+
+ return $this->pushObjectForm($type, $object, Request::input('formWidgetAlias'));
+ }
+
+ public function onGetInspectorConfiguration()
+ {
+ $configuration = [];
+
+ $snippetCode = Request::input('snippet');
+ $componentClass = Request::input('component');
+
+ if (strlen($snippetCode)) {
+ $snippet = SnippetManager::instance()->findByCodeOrComponent($this->theme, $snippetCode, $componentClass);
+ if (!$snippet) {
+ throw new ApplicationException(trans('rainlab.pages::lang.snippet.not_found', ['code' => $snippetCode]));
+ }
+
+ $configuration = $snippet->getProperties();
+ }
+
+ return [
+ 'configuration' => [
+ 'properties' => $configuration,
+ 'title' => $snippet->getName(),
+ 'description' => $snippet->getDescription()
+ ]
+ ];
+ }
+
+ public function onGetSnippetNames()
+ {
+ $codes = array_unique(Request::input('codes'));
+ $result = [];
+
+ foreach ($codes as $snippetCode) {
+ $parts = explode('|', $snippetCode);
+ $componentClass = null;
+
+ if (count($parts) > 1) {
+ $snippetCode = $parts[0];
+ $componentClass = $parts[1];
+ }
+
+ $snippet = SnippetManager::instance()->findByCodeOrComponent($this->theme, $snippetCode, $componentClass);
+
+ if (!$snippet) {
+ $result[$snippetCode] = trans('rainlab.pages::lang.snippet.not_found', ['code' => $snippetCode]);
+ }
+ else {
+ $result[$snippetCode] =$snippet->getName();
+ }
+ }
+
+ return [
+ 'names' => $result
+ ];
+ }
+
+ public function onMenuItemReferenceSearch()
+ {
+ $alias = Request::input('alias');
+
+ $widget = $this->makeFormWidget(
+ 'Rainlab\Pages\FormWidgets\MenuItemSearch',
+ [],
+ ['alias' => $alias]
+ );
+
+ return $widget->onSearch();
+ }
+
+ /**
+ * Commits the DB changes of a object to the filesystem
+ *
+ * @return array $response
+ */
+ public function onCommit()
+ {
+ $this->validateRequestTheme();
+ $type = Request::input('objectType');
+ $object = $this->loadObject($type, trim(Request::input('objectPath')));
+
+ if ($this->canCommitObject($object)) {
+ if (class_exists('System')) {
+ // v1.2
+ $datasource = $this->getThemeDatasource();
+ $datasource->updateModelAtIndex(1, $object);
+ $datasource->forceDeleteModelAtIndex(0, $object);
+ }
+ else {
+ // v1.1
+ $datasource = $this->getThemeDatasource();
+ $datasource->pushToSource($object, 'filesystem');
+ $datasource->removeFromSource($object, 'database');
+ }
+
+ Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type]));
+ }
+
+ return array_merge($this->getUpdateResponse($object, $type), ['forceReload' => true]);
+ }
+
+ /**
+ * Resets a object to the version on the filesystem
+ *
+ * @return array $response
+ */
+ public function onReset()
+ {
+ $this->validateRequestTheme();
+ $type = Request::input('objectType');
+ $object = $this->loadObject($type, trim(Request::input('objectPath')));
+
+ if ($this->canResetObject($object)) {
+ if (class_exists('System')) {
+ // v1.2
+ $datasource = $this->getThemeDatasource();
+ $datasource->forceDeleteModelAtIndex(0, $object);
+ }
+ else {
+ // v1.1
+ $datasource = $this->getThemeDatasource();
+ $datasource->removeFromSource($object, 'database');
+ }
+
+ Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type]));
+ }
+
+ return array_merge($this->getUpdateResponse($object, $type), ['forceReload' => true]);
+ }
+
+ //
+ // Methods for internal use
+ //
+
+ /**
+ * Get the response to return in an AJAX request that updates an object
+ *
+ * @param CmsObject $object The object that has been affected
+ * @param string $type The type of object being affected
+ * @return array $result;
+ */
+ protected function getUpdateResponse(CmsObject $object, string $type)
+ {
+ $result = [
+ 'objectPath' => $type != 'content' ? $object->getBaseFileName() : $object->fileName,
+ 'objectMtime' => $object->mtime,
+ 'tabTitle' => $this->getTabTitle($type, $object)
+ ];
+
+ if ($type == 'page') {
+ $result['pageUrl'] = Url::to($object->getViewBag()->property('url'));
+ PagesPlugin::clearCache();
+ }
+
+ $result['canCommit'] = $this->canCommitObject($object);
+ $result['canReset'] = $this->canResetObject($object);
+
+ return $result;
+ }
+
+ /**
+ * Get the active theme's datasource
+ */
+ protected function getThemeDatasource()
+ {
+ return $this->theme->getDatasource();
+ }
+
+ /**
+ * Check to see if the provided object can be committed
+ * Only available in debug mode, the DB layer must be enabled, and the object must exist in the database
+ *
+ * @param CmsObject $object
+ * @return boolean
+ */
+ protected function canCommitObject(CmsObject $object)
+ {
+ $result = false;
+
+ if (class_exists('System')) {
+ // v1.2
+ if (
+ Config::get('app.debug', false) &&
+ $this->theme->secondLayerEnabled() &&
+ $this->getThemeDatasource()->hasModelAtIndex(1, $object)
+ ) {
+ $result = true;
+ }
+ }
+ else {
+ // v1.1
+ if (Config::get('app.debug', false) &&
+ Theme::databaseLayerEnabled() &&
+ $this->getThemeDatasource()->sourceHasModel('database', $object)
+ ) {
+ $result = true;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Check to see if the provided object can be reset
+ * Only available when the DB layer is enabled and the object exists in both the DB & Filesystem
+ *
+ * @param CmsObject $object
+ * @return boolean
+ */
+ protected function canResetObject(CmsObject $object)
+ {
+ $result = false;
+
+ if (class_exists('System')) {
+ // v1.2
+ if ($this->theme->secondLayerEnabled()) {
+ $datasource = $this->getThemeDatasource();
+ $result = $datasource->hasModelAtIndex(0, $object) &&
+ $datasource->hasModelAtIndex(1, $object);
+ }
+ }
+ else {
+ // v1.1
+ if (Theme::databaseLayerEnabled()) {
+ $datasource = $this->getThemeDatasource();
+ $result = $datasource->sourceHasModel('database', $object) && $datasource->sourceHasModel('filesystem', $object);
+ }
+ }
+
+ return $result;
+ }
+
+ protected function validateRequestTheme()
+ {
+ if ($this->theme->getDirName() != Request::input('theme')) {
+ throw new ApplicationException(trans('cms::lang.theme.edit.not_match'));
+ }
+ }
+
+ protected function loadObject($type, $path, $ignoreNotFound = false)
+ {
+ $class = $this->resolveTypeClassName($type);
+
+ if (!($object = call_user_func(array($class, 'load'), $this->theme, $path))) {
+ if (!$ignoreNotFound) {
+ throw new ApplicationException(trans('rainlab.pages::lang.object.not_found'));
+ }
+
+ return null;
+ }
+
+ return $object;
+ }
+
+ protected function createObject($type)
+ {
+ $class = $this->resolveTypeClassName($type);
+
+ if (!($object = $class::inTheme($this->theme))) {
+ throw new ApplicationException(trans('rainlab.pages::lang.object.not_found'));
+ }
+
+ return $object;
+ }
+
+ protected function resolveTypeClassName($type)
+ {
+ $types = [
+ 'page' => 'RainLab\Pages\Classes\Page',
+ 'menu' => 'RainLab\Pages\Classes\Menu',
+ 'content' => 'RainLab\Pages\Classes\Content'
+ ];
+
+ if (!array_key_exists($type, $types)) {
+ throw new ApplicationException(Lang::get('rainlab.pages::lang.object.invalid_type') . ' - type - ' . $type);
+ }
+
+ $allowed = false;
+ if ($type === 'content') {
+ $allowed = $this->user->hasAccess('rainlab.pages.manage_content');
+ } else {
+ $allowed = $this->user->hasAccess("rainlab.pages.manage_{$type}s");
+ }
+
+ if (!$allowed) {
+ throw new ApplicationException(Lang::get('rainlab.pages::lang.object.unauthorized_type', ['type' => $type]));
+ }
+
+ return $types[$type];
+ }
+
+ protected function makeObjectFormWidget($type, $object, $alias = null)
+ {
+ $formConfigs = [
+ 'page' => '~/plugins/rainlab/pages/classes/page/fields.yaml',
+ 'menu' => '~/plugins/rainlab/pages/classes/menu/fields.yaml',
+ 'content' => '~/plugins/rainlab/pages/classes/content/fields.yaml'
+ ];
+
+ if (!array_key_exists($type, $formConfigs)) {
+ throw new ApplicationException(Lang::get('rainlab.pages::lang.object.not_found'));
+ }
+
+ $widgetConfig = $this->makeConfig($formConfigs[$type]);
+ $widgetConfig->model = $object;
+ $widgetConfig->alias = $alias ?: 'form' . studly_case($type) . md5($object->exists ? $object->getFileName() : uniqid());
+ $widgetConfig->context = !$object->exists ? 'create' : 'update';
+
+ $widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
+
+ if ($type == 'page') {
+ $widget->bindEvent('form.extendFieldsBefore', function() use ($widget, $object) {
+ $this->checkContentField($widget, $object);
+ $this->addPagePlaceholders($widget, $object);
+ $this->addPageSyntaxFields($widget, $object);
+ });
+ }
+
+ return $widget;
+ }
+
+ protected function checkContentField($formWidget, $page)
+ {
+ if (!($layout = $page->getLayoutObject())) {
+ return;
+ }
+
+ $component = $layout->getComponent('staticPage');
+
+ if (!$component) {
+ return;
+ }
+
+ if (!$component->property('useContent', true)) {
+ unset($formWidget->secondaryTabs['fields']['markup']);
+ }
+ }
+
+ protected function addPageSyntaxFields($formWidget, $page)
+ {
+ $fields = $page->listLayoutSyntaxFields();
+
+ foreach ($fields as $fieldCode => $fieldConfig) {
+ if ($fieldConfig['type'] == 'fileupload') continue;
+
+ if ($fieldConfig['type'] == 'repeater') {
+ if (empty($fieldConfig['form']) || !is_string($fieldConfig['form'])) {
+ $fieldConfig['form']['fields'] = array_get($fieldConfig, 'fields', []);
+ unset($fieldConfig['fields']);
+ }
+ }
+
+ /*
+ * Custom fields placement
+ */
+ $placement = (!empty($fieldConfig['placement']) ? $fieldConfig['placement'] : NULL);
+
+ switch ($placement) {
+ case 'primary':
+ $formWidget->tabs['fields']['viewBag[' . $fieldCode . ']'] = $fieldConfig;
+ break;
+
+ default:
+ $fieldConfig['cssClass'] = 'secondary-tab ' . array_get($fieldConfig, 'cssClass', '');
+ $formWidget->secondaryTabs['fields']['viewBag[' . $fieldCode . ']'] = $fieldConfig;
+ break;
+ }
+
+ /*
+ * Translation support
+ */
+ $translatableTypes = ['text', 'textarea', 'richeditor', 'repeater', 'markdown', 'mediafinder'];
+ if (in_array($fieldConfig['type'], $translatableTypes) && array_get($fieldConfig, 'translatable', true)) {
+ $page->translatable[] = 'viewBag['.$fieldCode.']';
+ }
+ }
+ }
+
+ protected function addPagePlaceholders($formWidget, $page)
+ {
+ $placeholders = $page->listLayoutPlaceholders();
+
+ foreach ($placeholders as $placeholderCode => $info) {
+ if ($info['ignore']) {
+ continue;
+ }
+
+ $placeholderTitle = $info['title'];
+ $fieldConfig = [
+ 'tab' => $placeholderTitle,
+ 'stretch' => '1',
+ 'size' => 'huge'
+ ];
+
+ if ($info['type'] != 'text') {
+ $fieldConfig['type'] = 'richeditor';
+ }
+ else {
+ $fieldConfig['type'] = 'codeeditor';
+ $fieldConfig['language'] = 'text';
+ $fieldConfig['theme'] = 'chrome';
+ $fieldConfig['showGutter'] = false;
+ $fieldConfig['highlightActiveLine'] = false;
+ $fieldConfig['cssClass'] = 'pagesTextEditor';
+ $fieldConfig['showInvisibles'] = false;
+ $fieldConfig['fontSize'] = 13;
+ $fieldConfig['margin'] = '20';
+ }
+
+ $formWidget->secondaryTabs['fields']['placeholders['.$placeholderCode.']'] = $fieldConfig;
+
+ /*
+ * Translation support
+ */
+ $page->translatable[] = 'placeholders['.$placeholderCode.']';
+ }
+ }
+
+ protected function getTabTitle($type, $object)
+ {
+ if ($type == 'page') {
+ $viewBag = $object->getViewBag();
+ $result = $viewBag ? $viewBag->property('title') : false;
+ if (!$result) {
+ $result = trans('rainlab.pages::lang.page.new');
+ }
+
+ return $result;
+ }
+ elseif ($type == 'menu') {
+ $result = $object->name;
+ if (!strlen($result)) {
+ $result = trans('rainlab.pages::lang.menu.new');
+ }
+
+ return $result;
+ }
+ elseif ($type == 'content') {
+ $result = in_array($type, ['asset', 'content'])
+ ? $object->getFileName()
+ : $object->getBaseFileName();
+
+ if (!$result) {
+ $result = trans('cms::lang.'.$type.'.new');
+ }
+
+ return $result;
+ }
+
+ return $object->getFileName();
+ }
+
+ protected function fillObjectFromPost($type)
+ {
+ $objectPath = trim(Request::input('objectPath'));
+ $object = $objectPath ? $this->loadObject($type, $objectPath) : $this->createObject($type);
+ $formWidget = $this->makeObjectFormWidget($type, $object, Request::input('formWidgetAlias'));
+
+ $saveData = $formWidget->getSaveData();
+ $postData = post();
+ $objectData = [];
+
+ if ($viewBag = array_get($saveData, 'viewBag')) {
+ $objectData['settings'] = ['viewBag' => $viewBag];
+ }
+
+ $fields = ['markup', 'code', 'fileName', 'content', 'itemData', 'name'];
+
+ if ($type != 'menu' && $type != 'content') {
+ $object->parentFileName = Request::input('parentFileName');
+ }
+
+ foreach ($fields as $field) {
+ if (array_key_exists($field, $saveData)) {
+ $objectData[$field] = $saveData[$field];
+ }
+ elseif (array_key_exists($field, $postData)) {
+ $objectData[$field] = $postData[$field];
+ }
+ }
+
+ if ($type == 'page') {
+ $placeholders = array_get($saveData, 'placeholders');
+
+ $comboConfig = Config::get('cms.convertLineEndings', Config::get('system.convert_line_endings', false));
+ if (is_array($placeholders) && $comboConfig === true) {
+ $placeholders = array_map([$this, 'convertLineEndings'], $placeholders);
+ }
+
+ $objectData['placeholders'] = $placeholders;
+ }
+
+ if ($type == 'content') {
+ $fileName = $objectData['fileName'];
+
+ if (dirname($fileName) == 'static-pages') {
+ throw new ApplicationException(trans('rainlab.pages::lang.content.cant_save_to_dir'));
+ }
+
+ $extension = pathinfo($fileName, PATHINFO_EXTENSION);
+
+ if ($extension === 'htm' || $extension === 'html' || !strlen($extension)) {
+ $objectData['markup'] = array_get($saveData, 'markup_html');
+ }
+ }
+
+ if ($type == 'menu') {
+ // If no item data is sent through POST, this means the menu is empty
+ if (!isset($objectData['itemData'])) {
+ $objectData['itemData'] = [];
+ } else {
+ $objectData['itemData'] = json_decode($objectData['itemData'], true);
+ if (json_last_error() !== JSON_ERROR_NONE || !is_array($objectData['itemData'])) {
+ $objectData['itemData'] = [];
+ }
+ }
+ }
+
+ $comboConfig = Config::get('cms.convertLineEndings', Config::get('system.convert_line_endings', false));
+ if (!empty($objectData['markup']) && $comboConfig === true) {
+ $objectData['markup'] = $this->convertLineEndings($objectData['markup']);
+ }
+
+ if (!Request::input('objectForceSave') && $object->mtime) {
+ if (Request::input('objectMtime') != $object->mtime) {
+ throw new ApplicationException('mtime-mismatch');
+ }
+ }
+
+ $object->fill($objectData);
+
+ /*
+ * Rehydrate the object viewBag array property where values are sourced.
+ */
+ if ($object instanceof CmsCompoundObject && is_array($viewBag)) {
+ $object->viewBag = $viewBag + $object->viewBag;
+ }
+
+ return $object;
+ }
+
+ protected function pushObjectForm($type, $object, $alias = null)
+ {
+ $widget = $this->makeObjectFormWidget($type, $object, $alias);
+
+ $this->vars['canCommit'] = $this->canCommitObject($object);
+ $this->vars['canReset'] = $this->canResetObject($object);
+ $this->vars['objectPath'] = Request::input('path');
+ $this->vars['lastModified'] = DateTime::makeCarbon($object->mtime);
+
+ if ($type == 'page') {
+ $this->vars['pageUrl'] = Url::to($object->getViewBag()->property('url'));
+ }
+
+ return [
+ 'tabTitle' => $this->getTabTitle($type, $object),
+ 'tab' => $this->makePartial('form_page', [
+ 'form' => $widget,
+ 'objectType' => $type,
+ 'objectTheme' => $this->theme->getDirName(),
+ 'objectMtime' => $object->mtime,
+ 'objectParent' => Request::input('parentFileName')
+ ])
+ ];
+ }
+
+ protected function bindFormWidgetToController()
+ {
+ $alias = Request::input('formWidgetAlias');
+ $type = Request::input('objectType');
+ $objectPath = trim(Request::input('objectPath'));
+
+ if (!$objectPath) {
+ $object = $this->createObject($type);
+
+ if ($type === 'page') {
+ /**
+ * If layout is in POST, populate that into the object's viewBag to allow placeholders and syntax
+ * fields to still work when editing a new page.
+ *
+ * Fixes https://github.com/octobercms/october/issues/4628
+ */
+ $layout = Request::input('viewBag.layout');
+ if ($layout) {
+ $object->getViewBag()->setProperty('layout', $layout);
+ }
+ }
+ } else {
+ $object = $this->loadObject($type, $objectPath);
+ }
+
+ $widget = $this->makeObjectFormWidget($type, $object, $alias);
+ $widget->bindToController();
+ }
+
+ /**
+ * Replaces Windows style (/r/n) line endings with unix style (/n)
+ * line endings.
+ * @param string $markup The markup to convert to unix style endings
+ * @return string
+ */
+ protected function convertLineEndings($markup)
+ {
+ $markup = str_replace("\r\n", "\n", $markup);
+ $markup = str_replace("\r", "\n", $markup);
+
+ return $markup;
+ }
+
+ /**
+ * Returns a list of content files
+ * @return \October\Rain\Database\Collection
+ */
+ protected function getContentTemplateList()
+ {
+ $templates = Content::listInTheme($this->theme, true);
+
+ /**
+ * @event pages.content.templateList
+ * Provides opportunity to filter the items returned to the ContentList widget used by the RainLab.Pages plugin in the backend.
+ *
+ * >**NOTE**: Recommended to just use cms.object.listInTheme instead
+ *
+ * Parameter provided is `$templates` (a collection of the Content CmsObjects being returned).
+ * > Note: The `$templates` parameter provided is an object reference to a CmsObjectCollection, to make changes you must use object modifying methods.
+ *
+ * Example usage (only shows allowed content files):
+ *
+ * \Event::listen('pages.content.templateList', function ($templates) {
+ * foreach ($templates as $index = $content) {
+ * if (!in_array($content->fileName, $allowedContent)) {
+ * $templates->forget($index);
+ * }
+ * }
+ * });
+ *
+ * Or:
+ *
+ * \RainLab\Pages\Controller\Index::extend(function ($controller) {
+ * $controller->bindEvent('content.templateList', function ($templates) {
+ * foreach ($templates as $index = $content) {
+ * if (!in_array($content->fileName, $allowedContent)) {
+ * $templates->forget($index);
+ * }
+ * }
+ * });
+ * });
+ * }
+ */
+ if (
+ ($event = $this->fireEvent('content.templateList', [$templates], true)) ||
+ ($event = Event::fire('pages.content.templateList', [$this, $templates], true))
+ ) {
+ return $event;
+ }
+
+ return $templates;
+ }
+}
diff --git a/plugins/rainlab/pages/controllers/index/_concurrency_resolve_form.htm b/plugins/rainlab/pages/controllers/index/_concurrency_resolve_form.htm
new file mode 100644
index 000000000..0586ab630
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/_concurrency_resolve_form.htm
@@ -0,0 +1,29 @@
+= Form::open(['onsubmit'=>'return false']) ?>
+
+
+
= e(trans('backend::lang.form.concurrency_file_changed_description')) ?>
+
+
+= Form::close() ?>
\ No newline at end of file
diff --git a/plugins/rainlab/pages/controllers/index/_content_toolbar.htm b/plugins/rainlab/pages/controllers/index/_content_toolbar.htm
new file mode 100644
index 000000000..5c07fdb2f
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/_content_toolbar.htm
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/controllers/index/_form_page.htm b/plugins/rainlab/pages/controllers/index/_form_page.htm
new file mode 100644
index 000000000..1ee8c7c1d
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/_form_page.htm
@@ -0,0 +1,25 @@
+= Form::open([
+ 'class' => 'layout',
+ 'data-change-monitor' => 'true',
+ 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')),
+ 'data-object-type' => e($objectType)
+]) ?>
+ = $form->render() ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+= Form::close() ?>
\ No newline at end of file
diff --git a/plugins/rainlab/pages/controllers/index/_menu_toolbar.htm b/plugins/rainlab/pages/controllers/index/_menu_toolbar.htm
new file mode 100644
index 000000000..db2973300
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/_menu_toolbar.htm
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/controllers/index/_page_toolbar.htm b/plugins/rainlab/pages/controllers/index/_page_toolbar.htm
new file mode 100644
index 000000000..719a5534c
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/_page_toolbar.htm
@@ -0,0 +1,35 @@
+
diff --git a/plugins/rainlab/pages/controllers/index/_sidepanel.htm b/plugins/rainlab/pages/controllers/index/_sidepanel.htm
new file mode 100644
index 000000000..fc58c6d90
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/_sidepanel.htm
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/rainlab/pages/controllers/index/config_content_list.yaml b/plugins/rainlab/pages/controllers/index/config_content_list.yaml
new file mode 100644
index 000000000..511203b45
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/config_content_list.yaml
@@ -0,0 +1,11 @@
+# ===================================
+# Configures the layout list widget
+# ===================================
+
+titleProperty: 'nice_title'
+noRecordsMessage: 'cms::lang.content.no_list_records'
+deleteConfirmation: 'cms::lang.content.delete_confirm_multiple'
+itemType: content
+controlClass: filelist-hero content
+ignoreDirectories:
+ - static-pages*
diff --git a/plugins/rainlab/pages/controllers/index/index.htm b/plugins/rainlab/pages/controllers/index/index.htm
new file mode 100644
index 000000000..e9279db10
--- /dev/null
+++ b/plugins/rainlab/pages/controllers/index/index.htm
@@ -0,0 +1,32 @@
+= Block::put('sidepanel') ?>
+ fatalError): ?>
+ = $this->makePartial('sidepanel') ?>
+
+= Block::endPut() ?>
+
+= Block::put('body') ?>
+ fatalError): ?>
+
+
+
+ = e(trans($this->fatalError)) ?>
+
+= Block::endPut() ?>
diff --git a/plugins/rainlab/pages/docs/component-childpages.md b/plugins/rainlab/pages/docs/component-childpages.md
new file mode 100644
index 000000000..55268c196
--- /dev/null
+++ b/plugins/rainlab/pages/docs/component-childpages.md
@@ -0,0 +1,35 @@
+# Component: Child Pages (childPages)
+
+## Purpose
+Outputs a list of child pages of the current page
+
+## Default output
+
+The default component partial outputs a simple nested unordered list:
+
+```html
+
+```
+
+You might want to render the list with your own code. The `childPages.pages` variable is an array of arrays representing the child pages. Each of the arrays has the following items:
+
+Property | Type | Description
+-------- | ---- | -----------
+`url` | `string` | The relative URL for the page (use `{{ url | app }}` to get the absolute URL)
+`title` | `string` | Page title
+`page` | `RainLab\Pages\Classes\Page` | The page object itself
+`viewBag` | `array` | Contains all the extra data used by the page
+`is_hidden` | `bool` | Whether the page is hidden (only accessible to backend users)
+`navigation_hidden` | `bool` | Whether the page is hidden in automaticaly generated contexts (i.e menu)
+
+## Example of custom markup for component
+
+```html
+{% for page in childPages.pages %}
+ {{ page.title }}
+{% endfor %}
+```
\ No newline at end of file
diff --git a/plugins/rainlab/pages/docs/component-staticbreadcrumbs.md b/plugins/rainlab/pages/docs/component-staticbreadcrumbs.md
new file mode 100644
index 000000000..27db415a4
--- /dev/null
+++ b/plugins/rainlab/pages/docs/component-staticbreadcrumbs.md
@@ -0,0 +1,44 @@
+# Component: Static Menu (staticMenu)
+
+## Purpose
+Outputs a breadcrumb navigation for the current static page
+
+## Page variables
+
+Variable | Type | Description
+-------- | ---- | -----------
+`breadcrumbs` | `array` | Array of `RainLab\Pages\Classes\MenuItemReference` objects representing the defined menu
+
+## Default output
+
+The default component partial outputs a simple unordered list for breadcrumbs:
+
+```twig
+{% if breadcrumbs %}
+
+{% endif %}
+```
+
+You might want to render the breadcrumbs with your own code. The `breadcrumbs` variable is an array of the `RainLab\Pages\Classes\MenuItemReference` objects. Each object has the following properties:
+
+Property | Type | Description
+-------- | ---- | -----------
+`title` | `string` | Menu item title
+`url` | `string` | Absolute menu item URL
+`isActive` | `bool` | Indicates whether the item corresponds to a page currently being viewed
+`isChildActive` | `bool` | Indicates whether the item contains an active subitem.
+`items` | `array` | The menu item subitems, if any. If there are no subitems, the array is empty
+
+## Example of custom markup for component
+
+```html
+{% for item in staticBreadCrumbs.breadcrumbs %}
+ {{ item.title }}
+{% endfor %}
+```
\ No newline at end of file
diff --git a/plugins/rainlab/pages/docs/component-staticmenu.md b/plugins/rainlab/pages/docs/component-staticmenu.md
new file mode 100644
index 000000000..d7d236086
--- /dev/null
+++ b/plugins/rainlab/pages/docs/component-staticmenu.md
@@ -0,0 +1,65 @@
+# Component: Static Menu (staticMenu)
+
+## Purpose
+Outputs a single menu
+
+## Available properties:
+
+Property | Inspector Name | Description
+-------- | -------------- | -----------
+`code` | Menu | The code (identifier) for the menu that should be displayed by the component
+
+## Page variables
+
+Variable | Type | Description
+-------- | ---- | -----------
+`menuItems` | `array` | Array of `RainLab\Pages\Classes\MenuItemReference` objects representing the defined menu
+
+## Default output
+
+The default component partial outputs a simple nested unordered list for menus:
+
+```html
+
+```
+
+You might want to render the menus with your own code. The `menuItems` variable is an array of the `RainLab\Pages\Classes\MenuItemReference` objects. Each object has the following properties:
+
+Property | Type | Description
+-------- | ---- | -----------
+`title` | `string` | Menu item title
+`url` | `string` | Absolute menu item URL
+`isActive` | `bool` | Indicates whether the item corresponds to a page currently being viewed
+`isChildActive` | `bool` | Indicates whether the item contains an active subitem.
+`items` | `array` | The menu item subitems, if any. If there are no subitems, the array is empty
+
+## Example of custom markup for component
+
+```html
+{% for item in staticMenu.menuItems %}
+ {{ item.title }}
+{% endfor %}
+```
+
+## Setting the active menu item explicitly
+
+In some cases you might want to mark a specific menu item as active explicitly. You can do that in the page's [`onInit()`](https://octobercms.com/docs/cms/pages#dynamic-pages) function with assigning the `activeMenuItem` page variable a value matching the menu item code you want to make active. Menu item codes are managed in the Edit Menu Item popup.
+
+```php
+function onInit()
+{
+ $this['activeMenuItem'] = 'blog';
+}
+```
\ No newline at end of file
diff --git a/plugins/rainlab/pages/docs/component-staticpage.md b/plugins/rainlab/pages/docs/component-staticpage.md
new file mode 100644
index 000000000..5c428317a
--- /dev/null
+++ b/plugins/rainlab/pages/docs/component-staticpage.md
@@ -0,0 +1,39 @@
+# Component: Static Page (staticPage)
+
+## Purpose
+Enables Static Pages to use the layout that includes this component.
+
+## Available properties
+
+Property | Inspector Name | Description
+-------- | -------------- | -----------
+`useContent` | Use page content field | If false, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.
+`default` | Default layout | If true, defines this layout (the layout this component is included on) as the default for new pages
+`childLayout` | Subpage layout | The layout to use as the default for any new subpages created from pages that use this layout
+
+## Page variables
+
+Variable | Type | Description
+-------- | ---- | -----------
+`page` | `RainLab\Pages\Classes\Page` | Reference to the current static page object
+`title` | `string` | The title of the current static page
+`extraData` | `array` | Any extra data defined in the page object (i.e. placeholders & variables defined in the layout)
+
+## Default output
+
+The default component partial outputs the rendered contents of the current Static Page. However, it's recommended to just use `{% page %}` to render the contents of the page instead to match up with how CMS pages are rendered.
+
+## Default page layout
+
+If adding a new subpage, the parent page's layout is checked for a `childLayout` property, and the new subpage's layout will default to that property value. Otherwise, the theme layouts will be searched for the `default` component property and that layout will be selected by default.
+
+Example:
+```
+# /themes/mytheme/layouts/layout1.htm
+[staticPage]
+default = true
+childLayout = "child"
+
+# /themes/mytheme/layouts/child.htm
+[staticPage]
+```
\ No newline at end of file
diff --git a/plugins/rainlab/pages/docs/documentation.md b/plugins/rainlab/pages/docs/documentation.md
new file mode 100644
index 000000000..54c04b8ea
--- /dev/null
+++ b/plugins/rainlab/pages/docs/documentation.md
@@ -0,0 +1,464 @@
+The plugin currently includes three components: Static Page, Static Menu and Static Breadcrumbs.
+
+### Integrating the Static Pages plugin
+
+In the simplest case you could create a [layout](https://octobercms.com/docs/cms/layouts) in the CMS area and include the plugin's components to its body. The next example layout outputs a menu, breadcrumbs and a static page:
+
+
+
+ {{ this.page.title }}
+
+
+ {% component 'staticMenu' %}
+ {% component 'staticBreadcrumbs' %}
+ {% page %}
+
+
+
+ {.img-responsive .frame}
+
+##### Static pages
+
+Include the Static Page [component](http://octobercms.com/docs/cms/components) to the layout. The Static Page component has two public properties:
+
+* `title` - specifies the static page title.
+* `content` - the static page content.
+
+##### Static menus
+
+Add the staticMenu component to the static page layout to output a menu. The static menu component has the `code` property that should refer a code of a static menu the component should display. In the Inspector the `code` field is displayed as Menu.
+
+The static menu component injects the `menuItems` page variable. The default component partial outputs a simple nested unordered list for menus:
+
+
+
+You might want to render the menus with your own code. The `menuItems` variable is an array of the `RainLab\Pages\Classes\MenuItemReference` objects. Each object has the following properties:
+
+* `title` - specifies the menu item title.
+* `url` - specifies the absolute menu item URL.
+* `isActive` - indicates whether the item corresponds to a page currently being viewed.
+* `isChildActive` - indicates whether the item contains an active subitem.
+* `items` - an array of the menu item subitems, if any. If there are no subitems, the array is empty
+
+The static menu component also has the `menuItems` property that you can access in the Twig code using the component's alias, for example:
+
+ {% for item in staticMenu.menuItems %}
+ {{ item.title }}
+ {% endfor %}
+
+##### Breadcrumbs
+
+The staticBreadcrumbs component outputs breadcrumbs for static pages. This component doesn't have any properties. The default component partial outputs a simple unordered list for the breadcrumbs:
+
+
+
+The component injects the `breadcrumbs` page variable that contains an array of the `MenuItemReference` objects described above.
+
+##### Setting the active menu item explicitly
+
+In some cases you might want to mark a specific menu item as active explicitly. You can do that in the page's [`onInit()`](http://octobercms.com/docs/cms/pages#dynamic-pages) function with assigning the `activeMenuItem` page variable a value matching the menu item code you want to make active. Menu item codes are managed in the Edit Menu Item popup.
+
+ function onInit()
+ {
+ $this['activeMenuItem'] = 'blog';
+ }
+
+##### Linking to static pages
+
+When a static page is first created it will be assigned a file name based on the URL. For example, a page with the URL **/chairs** will create a content file called **static-pages/chairs.htm** in the theme. This file will not change even if the URL is changed at a later time.
+
+To create a link to a static page, use the `|staticPage` filter:
+
+ Go to Chairs
+
+This filter translates to PHP code as:
+
+ echo RainLab\Pages\Classes\Page::url('chairs');
+
+If you want to link to the static page by its URL, simply use the `|app` filter:
+
+ Go to Chairs
+
+##### Manually displaying a static menu
+
+When a static menu is first created it will be assigned a file name based on the menu name (menu code can also be manually defined). For example, a menu with the name **Primary Nav** will create a meta file called **menus/primary-nav.yaml** in the theme. This file will not change even if the menu name is changed at a later time.
+
+To render a static menu based on a menu code from the `staticmenupicker` dropdown form widget:
+
+You can either define the code property on the staticMenu component.
+
+ {% component 'staticMenu' code=this.theme.primary_menu %}
+
+Or, use the resetMenu method on the staticMenu component, so we can manually control the menu output without having to create a staticMenu partial override.
+
+```twig
+{% set menuItems = staticMenu.resetMenu(this.theme.primary_menu) %}
+
+
+```
+
+##### Backend forms
+
+If you need to select from a list of static pages in your own backend forms, you can use the `staticpagepicker` widget:
+
+ fields:
+ field_name:
+ label: Static Page
+ type: staticpagepicker
+
+The field's assigned value will be the static page's file name, which can be used to link to the page as described above.
+
+If you need to select from a list of static menus in your own backend forms, you can use the `staticmenupicker` widget:
+
+ fields:
+ field_name:
+ label: Static Menu
+ type: staticmenupicker
+
+The field's assigned value will be the static menu's code, which can be used to link to the menu as described above.
+
+### Placeholders
+
+[Placeholders](https://octobercms.com/docs/cms/layouts#placeholders) defined in the layout are automatically detected by the Static Pages plugin. The Edit Static Page form displays a tab for each placeholder defined in the layout used by the page. Placeholders are defined in the layout in the usual way:
+
+ {% placeholder ordering %}
+
+The `placeholder` tag accepts some optional attributes:
+
+- `title`: manages the tab title in the Static Page editor.
+- `type`: manages the placeholder type. There are two types supported at the moment - **text** and **html**.
+- `ignore`: if set to true, will be ignored by the Static Page editor.
+
+The content of text placeholders is escaped before it's displayed. Text placeholders are edited with a regular (non-WYSIWYG) text editor. The title and type attributes should be defined after the placeholder code:
+
+ {% placeholder ordering title="Ordering information" type="text" %}
+
+They should also appear after the `default` attribute, if it's presented.
+
+ {% placeholder ordering default title="Ordering information" type="text" %}
+ There is no ordering information for this product.
+ {% endplaceholder %}
+
+To prevent a placeholder from appearing in the editor set the `ignore` attribute.
+
+ {% placeholder systemInfo ignore=true %}
+
+### Creating new menu item types
+
+Plugins can extend the Static Pages plugin with new menu item types. Please refer to the [Blog plugin](https://octobercms.com/plugin/rainlab-blog) for the integration example. New item types are registered with the API events triggered by the Static Pages plugin. The event handlers should be defined in the `boot()` method of the [plugin registration file](https://octobercms.com/docs/plugin/registration#registration-file). There are three events that should be handled in the plugin.
+
+* `pages.menuitem.listType` event handler should return a list of new menu item types supported by the plugin.
+* `pages.menuitem.getTypeInfo` event handler returns detailed information about a menu item type.
+* `pages.menuitem.resolveItem` event handler "resolves" a menu item information and returns the actual item URL, title, an indicator whether the item is currently active, and subitems, if any.
+
+The next example shows an event handler registration code for the Blog plugin. The Blog plugin registers two item types. As you can see, the Blog plugin uses the Category class to handle the events. That's a recommended approach.
+
+ public function boot()
+ {
+ Event::listen('pages.menuitem.listTypes', function() {
+ return [
+ 'blog-category'=>'Blog category',
+ 'all-blog-categories'=>'All blog categories',
+ ];
+ });
+
+ Event::listen('pages.menuitem.getTypeInfo', function($type) {
+ if ($type == 'blog-category' || $type == 'all-blog-categories') {
+ return Category::getMenuTypeInfo($type);
+ }
+ });
+
+ Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) {
+ if ($type == 'blog-category' || $type == 'all-blog-categories') {
+ return Category::resolveMenuItem($item, $url, $theme);
+ }
+ });
+ }
+
+##### Registering new menu item types
+
+New menu item types are registered with the `pages.menuitem.listTypes` event handlers. The handler should return an associative array with the type codes in indexes and type names in values. It's highly recommended to use the plugin name in the type codes, to avoid conflicts with other menu item type providers. Example:
+
+ [
+ `my-plugin-item-type` => 'My plugin menu item type'
+ ]
+
+##### Returning information about an item type
+
+Plugins should provide detailed information about the supported menu item types with the `pages.menuitem.getTypeInfo` event handlers. The handler gets a single parameter - the menu item type code (one of the codes you registered with the `pages.menuitem.listTypes` handler). The handler code must check whether the requested item type code belongs to the plugin. The handler should return an associative array in the following format:
+
+ Array (
+ [dynamicItems] => 0,
+ [nesting] => 0,
+ [references] => Array (
+ [11] => News,
+ [12] => Tutorials,
+ [33] => Philosophy
+ )
+ [cmsPages] => Array (
+ [0] => Cms\Classes\Page object,
+ [1] => Cms\Classes\Page object
+ )
+ )
+
+All elements of the array are optional and depend on the menu item type. The default values for `dynamicItems` and `nesting` are `false` and these keys can be omitted.
+
+###### dynamicItems element
+
+The `dynamicItems` element is a Boolean value indicating whether the item type could generate new menu items. Optional, false if omitted. Examples of menu item types that generate new menu items: **All blog categories**, **Static page**. Examples of item types that don't generate new menu items: **URL**, **Blog category**.
+
+###### nesting element
+
+The `nesting` element is a Boolean value indicating whether the item type supports nested items. Optional, `false` if omitted. Examples of item types that support nesting: **Static page**, **All static pages**. Examples of item types that don't support nesting: **Blog category**, **URL**.
+
+###### references element
+
+The `references` element is a list objects the menu item could refer to. For example, the **Blog category** menu item type returns a list of the blog categories. Some object supports nesting, for example static pages. Other objects don't support nesting, for example the blog categories. The format of the `references` value depends on whether the references have subitems or not. The format for references that don't support subitems is
+
+ ['item-key' => 'Item title']
+
+The format for references with subitems is
+
+ ['item-key' => ['title'=>'Item title', 'items'=>[...]]]
+
+The reference keys should reflect the object identifier they represent. For blog categories keys match the category identifiers. A plugin should be able to load an object by its key in the `pages.menuitem.resolveItem` event handler. The references element is optional, it is required only if a menu item type supports the Reference drop-down, or, in other words, if the user should be able to select an object the menu item refers to.
+
+###### cmsPages element
+
+The `cmsPages` is a list of CMS pages that can display objects supported by the menu item type. For example, for the **Blog category** item type the page list contains pages that host the `blogPosts` component. That component can display a blog category contents. The `cmsPages` element should be an array of the `Cms\Classes\Page` objects. The next code snippet shows how to return a list of pages hosting a specific component.
+
+ use Cms\Classes\Page as CmsPage;
+ use Cms\Classes\Theme;
+
+ ...
+
+ $result = [];
+ ...
+ $theme = Theme::getActiveTheme();
+ $pages = CmsPage::listInTheme($theme, true);
+
+ $cmsPages = [];
+ foreach ($pages as $page) {
+ if (!$page->hasComponent('blogPosts')) {
+ continue;
+ }
+
+ $cmsPages[] = $page;
+ }
+
+ $result['cmsPages'] = $cmsPages;
+ ...
+ return $result;
+
+##### Resolving menu items
+
+When the Static Pages plugin generates a menu on the front-end, every menu item should **resolved** by the plugin that supplies the menu item type. The process of resolving involves generating the real item URL, determining whether the menu item is active, and generating the subitems (if required). Plugins should register the `pages.menuitem.resolveItem` event handler in order to resolve menu items. The event handler takes four arguments:
+
+* `$type` - the item type name. Plugins must only handle item types they provide and ignore other types.
+* `$item` - the menu item object (RainLab\Pages\Classes\MenuItem). The menu item object represents the menu item configuration provided by the user. The object has the following properties: `title`, `type`, `reference`, `cmsPage`, `nesting`.
+* `$url` - specifies the current absolute URL, in lower case. Always use the `Url::to()` helper to generate menu item links and compare them with the current URL.
+* `$theme` - the current theme object (`Cms\Classes\Theme`).
+
+The event handler should return an array. The array keys depend on whether the menu item contains subitems or not. Expected result format:
+
+ Array (
+ [url] => https://example.com/blog/category/another-category
+ [isActive] => 1,
+ [items] => Array (
+ [0] => Array (
+ [title] => Another category
+ [url] => https://example.com/blog/category/another-category
+ [isActive] => 1
+ )
+
+ [1] => Array (
+ [title] => News
+ [url] => https://example.com/blog/category/news
+ [isActive] => 0
+ )
+ )
+ )
+
+The `url` and `isActive` elements are required for menu items that point to a specific page, but it's not always the case. For example, the **All blog categories** menu item type doesn't have a specific page to point to. It generates multiple menu items. In this case the items should be listed in the `items` element. The `items` element should only be provided if the menu item's `nesting` property is `true`.
+
+As the resolving process occurs every time when the front-end page is rendered, it's a good idea to cache all the information required for resolving menu items, if that's possible.
+
+If your item type requires a CMS page to resolve item URLs, you might need to return the selected page's URL, and sometimes pass parameters to the page through the URL. The next code example shows how to load a blog category CMS page referred by a menu item and how to generate an URL to this page. The blog category page has the `blogPosts` component that can load the requested category slug from the URL. We assume that the URL parameter is called 'slug', although it can be edited manually. We skip the part that loads the real parameter name for the simplicity. Please refer to the [Blog plugin](https://octobercms.com/plugin/rainlab-blog) for the reference.
+
+ use Cms\Classes\Page as CmsPage;
+ use October\Rain\Router\Helper as RouterHelper;
+ use Str;
+ use Url;
+
+ ...
+
+ $page = CmsPage::loadCached($theme, $item->cmsPage);
+
+ // Always check if the page can be resolved
+ if (!$page) {
+ return;
+ }
+
+ // Generate the URL
+ $url = CmsPage::url($page->getBaseFileName(), ['slug' => $category->slug]);
+
+ $url = Url::to(Str::lower(RouterHelper::normalizeUrl($url)));
+
+To determine whether an item is active just compare it with the `$url` argument of the event handler.
+
+#### Overriding generated references
+
+In order to override generated references you can listen to `pages.menu.referencesGenerated` event that fires right before injecting to page object. For example you can filter the unwanted menu entries.
+
+#### Snippets
+
+Snippets are elements that can be added by non-technical user to a Static Page, in the rich text editor. They allow to inject complex (and interactive) areas to pages. There are many possible applications and examples of using Snippets:
+
+* Google Maps snippet - outputs a map centered on specific coordinates with predefined zoom factor. That snippet would be great for static pages that explain directions.
+* Universal commenting system - allows visitors to post comments to any static page.
+* Third-party integrations - for example with Yelp or TripAdvisor for displaying extra information on a static page.
+
+Snippets are displayed in the sidebar list on the Static Pages and can be added into a rich editor with a mouse click. Snippets are configurable and have properties that users can manage with the Inspector.
+
+Snippets can be created from partials or programmatically in plugins. Conceptually snippets are similar to CMS components (and technically, components can act as snippets).
+
+###### Snippets created from partials
+
+Partial-based snippets provide simpler functionality and usually are just containers for HTML markup (or markup generated with Twig in a snippet).
+
+To create snippet from a partial just enter the snippet code and snippet name in the partial form.
+
+
+
+The snippet properties are optional and can be defined with the grid control on the partial settings form. The table has the following columns:
+
+* Property title - specifies the property title. The property title will be visible to the end user in the snippet inspector popup window.
+* Property code - specifies the property code. The property code is used for accessing the property values in the partial markup. See the example below. The property code should start with a Latin letter and can contain Latin letters and digits.
+* Type - the property type. Available types are String, Dropdown and Checkbox.
+* Default - the default property value. For Checkbox properties use 0 and 1 values.
+* Options - the option list for the drop-down properties. The option list should have the following format: `key:Value | key2:Value`. The keys represent the internal option value, and values represent the string that users see in the drop-down list. The pipe character separates individual options. Example: `us:US | ca:Canada`. The key is optional, if it's omitted (`US | Canada`), the internal option value will be zero-based integer (0, 1, ...). It's recommended to always use explicit option keys. The keys can contain only Latin letters, digits and characters - and _.
+
+Any property defined in the property list can be accessed within the partial markdown as a usual variable, for example:
+
+ The country name is {{ country }}
+
+In addition, properties can be passed to the partial components using an [external property value](http://octobercms.com/docs/cms/components#external-property-values).
+
+###### Snippets created from components
+
+Any component can be registered as a snippet and be used in Static Pages. To register a snippet, add the `registerPageSnippets()` method to your plugin class in the [registration file](https://octobercms.com/docs/plugin/registration#registration-file). The API for registering a snippet is similar to the one for [registering components](https://octobercms.com/docs/plugin/registration#component-registration) - the method should return an array with class names in keys and aliases in values:
+
+ public function registerPageSnippets()
+ {
+ return [
+ '\RainLab\Weather\Components\Weather' => 'weather'
+ ];
+ }
+
+A same component can be registered with registerPageSnippets() and registerComponents() and used in CMS pages and Static Pages.
+
+###### Extending the list of snippets
+
+If you want to dynamically extend the list of the snippets you can bind to the `pages.snippets.listSnippets` event.
+
+An example usage to add a snippet to the list:
+
+ Event::listen('pages.snippets.listSnippets', function($manager) {
+ $snippet = new \RainLab\Pages\Classes\Snippet();
+ $snippet->initFromComponentInfo('\Example\Plugin\Components\ComponentClass', 'snippetCode');
+ $manager->addSnippet($snippet);
+ });
+
+An example usage to remove a snippet from the list:
+
+ Event::listen('pages.snippets.listSnippets', function($manager) {
+ $manager->removeSnippet('snippetCode');
+ });
+
+##### Custom page fields
+
+There is a special syntax you can use inside your layout to add custom fields to the page editor form, called *Syntax Fields*. For example, if you add the following markup to a Layout that uses Static Pages:
+
+ {variable name="tagline" label="Tagline" tab="Header" type="text"}{/variable}
+ {variable name="banner" label="Banner" tab="Header" type="mediafinder" mode="image"}{/variable}
+ {variable name="color" label="Color" tab="Header" type="dropdown"
+ options="blue:Blue | orange:Orange | red:Red"
+ }{/variable}
+
+These act just like regular form field definitions. Accessing the variables inside the markup is just as easy:
+
+ {{ tagline }}
+
+
+All custom fields are placed in the Secondary tabs container (next to Content field). If you need to place them in the Primary tabs container, use *placement="primary"* attribute.
+
+ {variable name="tagline" label="Tagline" tab="Header" type="text" placement="primary"}{/variable}
+
+Alternatively you may use the field type as the tag name, here we use the `{text}` tag to directly render the `tagline` variable:
+
+ {text name="tagline" label="Tagline"}Our wonderful website{/text}
+
+You may also use the `{repeater}` tag for repeating content:
+
+ {repeater name="content_sections" prompt="Add another content section"}
+
+ {text name="content_header" label="Content section" placeholder="Type in a heading and enter some content for it below"}{/text}
+
+
+ {richeditor name="content_body" size="large"}{/richeditor}
+
+ {/repeater}
+
+For more details on syntax fields, see the [Parser section](https://octobercms.com/docs/services/parser#dynamic-syntax-parser) of the October documentation.
+
+##### Custom menu item form fields
+
+Just like CMS objects have the view bag component to store arbitrary values, you may use the `viewBag` property of the `MenuItem` class to store custom data values and add corresponding form fields.
+
+ Event::listen('backend.form.extendFields', function ($widget) {
+ if (
+ !$widget->getController() instanceof \RainLab\Pages\Controllers\Index ||
+ !$widget->model instanceof \RainLab\Pages\Classes\MenuItem
+ ) {
+ return;
+ }
+
+ $widget->addTabFields([
+ 'viewBag[featured]' => [
+ 'tab' => 'Display',
+ 'label' => 'Featured',
+ 'comment' => 'Mark this menu item as featured',
+ 'type' => 'checkbox'
+ ]
+ ]);
+ });
+
+This value can then be accessed in Twig using the `{{ item.viewBag }}` property on the menu item. For example:
+
+ {% for item in items %}
+
+
+ {{ item.title }}
+
+
+ {% endfor %}
+
diff --git a/plugins/rainlab/pages/docs/images/menu-item.png b/plugins/rainlab/pages/docs/images/menu-item.png
new file mode 100644
index 000000000..fc207e2e8
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/menu-item.png differ
diff --git a/plugins/rainlab/pages/docs/images/menu-management.png b/plugins/rainlab/pages/docs/images/menu-management.png
new file mode 100644
index 000000000..76e8a8bbd
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/menu-management.png differ
diff --git a/plugins/rainlab/pages/docs/images/snippets-backend.png b/plugins/rainlab/pages/docs/images/snippets-backend.png
new file mode 100644
index 000000000..2e415fc03
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/snippets-backend.png differ
diff --git a/plugins/rainlab/pages/docs/images/snippets-frontend.png b/plugins/rainlab/pages/docs/images/snippets-frontend.png
new file mode 100644
index 000000000..4e27130f3
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/snippets-frontend.png differ
diff --git a/plugins/rainlab/pages/docs/images/snippets-partial.png b/plugins/rainlab/pages/docs/images/snippets-partial.png
new file mode 100644
index 000000000..999353afe
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/snippets-partial.png differ
diff --git a/plugins/rainlab/pages/docs/images/static-layout.png b/plugins/rainlab/pages/docs/images/static-layout.png
new file mode 100644
index 000000000..dde2baa8b
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/static-layout.png differ
diff --git a/plugins/rainlab/pages/docs/images/static-page.png b/plugins/rainlab/pages/docs/images/static-page.png
new file mode 100644
index 000000000..782a352ac
Binary files /dev/null and b/plugins/rainlab/pages/docs/images/static-page.png differ
diff --git a/plugins/rainlab/pages/formwidgets/MenuItemSearch.php b/plugins/rainlab/pages/formwidgets/MenuItemSearch.php
new file mode 100644
index 000000000..d1bf49dad
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/MenuItemSearch.php
@@ -0,0 +1,105 @@
+makePartial('body');
+ }
+
+ /*
+ * Event handlers
+ */
+ public function onSearch()
+ {
+ $this->setSearchTerm(Input::get('term'));
+
+ return $this->getData();
+ }
+
+ /*
+ * Methods for internal use
+ */
+ protected function getData()
+ {
+ return [
+ 'results' => $this->getMatches()
+ ];
+ }
+
+ protected function getMatches()
+ {
+ $searchTerm = Str::lower($this->getSearchTerm());
+ if (!strlen($searchTerm)) {
+ return [];
+ }
+
+ $words = explode(' ', $searchTerm);
+
+ $iterator = function ($type, $references) use (&$iterator, $words) {
+ $typeMatches = [];
+
+ foreach ($references as $key => $referenceInfo) {
+ $title = (is_array($referenceInfo))
+ ? $referenceInfo['title']
+ : $referenceInfo;
+
+ if ($this->textMatchesSearch($words, $title)) {
+ $typeMatches[] = [
+ 'id' => "$type::$key",
+ 'text' => $title
+ ];
+ }
+
+ if (isset($referenceInfo['items']) && count($referenceInfo['items'])) {
+ $typeMatches = array_merge($typeMatches, $iterator($type, $referenceInfo['items']));
+ }
+ }
+
+ return $typeMatches;
+ };
+
+ $types = [];
+ $item = new MenuItem();
+ foreach ($item->getTypeOptions() as $type => $typeTitle) {
+ $typeInfo = MenuItem::getTypeInfo($type);
+ if (empty($typeInfo['references'])) {
+ continue;
+ }
+
+ $typeMatches = $iterator($type, $typeInfo['references']);
+
+ if (!empty($typeMatches)) {
+ $types[] = [
+ 'text' => trans($typeTitle),
+ 'children' => $typeMatches
+ ];
+ }
+ }
+
+ return $types;
+ }
+}
diff --git a/plugins/rainlab/pages/formwidgets/MenuItems.php b/plugins/rainlab/pages/formwidgets/MenuItems.php
new file mode 100644
index 000000000..775826825
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/MenuItems.php
@@ -0,0 +1,175 @@
+prepareVars();
+
+ return $this->makePartial('menuitems');
+ }
+
+ /**
+ * Prepares the list data
+ */
+ public function prepareVars()
+ {
+ $menuItem = new MenuItem;
+
+ $this->vars['itemProperties'] = json_encode($menuItem->fillable);
+ $this->vars['items'] = $this->model->items;
+
+ $emptyItem = new MenuItem;
+ $emptyItem->title = trans($this->newItemTitle);
+ $emptyItem->type = 'url';
+ $emptyItem->url = '/';
+
+ $this->vars['emptyItem'] = $emptyItem;
+
+ $widgetConfig = $this->makeConfig('~/plugins/rainlab/pages/classes/menuitem/fields.yaml');
+ $widgetConfig->model = $menuItem;
+ $widgetConfig->alias = $this->alias.'MenuItem';
+
+ $this->vars['itemFormWidget'] = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function loadAssets()
+ {
+ $this->addJs('js/menu-items-editor.js', 'RainLab.Pages');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getSaveValue($value)
+ {
+ return strlen($value) ? $value : null;
+ }
+
+ //
+ // Methods for the internal use
+ //
+
+ /**
+ * Returns the item reference description.
+ * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item
+ * @return string
+ */
+ protected function getReferenceDescription($item)
+ {
+ if ($this->typeListCache === false) {
+ $this->typeListCache = $item->getTypeOptions();
+ }
+
+ if (!isset($this->typeInfoCache[$item->type])) {
+ $this->typeInfoCache[$item->type] = MenuItem::getTypeInfo($item->type);
+ }
+
+ if (isset($this->typeInfoCache[$item->type])) {
+ $result = trans($this->typeListCache[$item->type]);
+
+ if ($item->type !== 'url') {
+ if (isset($this->typeInfoCache[$item->type]['references'])) {
+ $result .= ': '.$this->findReferenceName($item->reference, $this->typeInfoCache[$item->type]['references']);
+ }
+ }
+ else {
+ $result .= ': '.$item->url;
+ }
+
+ }
+ else {
+ $result = trans('rainlab.pages::lang.menuitem.unknown_type');
+ }
+
+ return $result;
+ }
+
+ protected function findReferenceName($search, $typeOptionList)
+ {
+ $iterator = function($optionList, $path) use ($search, &$iterator) {
+ foreach ($optionList as $reference => $info) {
+ if ($reference == $search) {
+ $result = $this->getMenuItemTitle($info);
+
+ return strlen($path) ? $path.' / ' .$result : $result;
+ }
+
+ if (is_array($info) && isset($info['items'])) {
+ $result = $iterator($info['items'], $path.' / '.$this->getMenuItemTitle($info));
+
+ if (strlen($result)) {
+ return strlen($path) ? $path.' / '.$result : $result;
+ }
+ }
+ }
+ };
+
+ $result = $iterator($typeOptionList, null);
+ if (!strlen($result)) {
+ $result = trans('rainlab.pages::lang.menuitem.unnamed');
+ }
+
+ $result = preg_replace('|^\s+\/|', '', $result);
+
+ return $result;
+ }
+
+ protected function getMenuItemTitle($itemInfo)
+ {
+ if (is_array($itemInfo)) {
+ if (!array_key_exists('title', $itemInfo) || !strlen($itemInfo['title'])) {
+ return trans('rainlab.pages::lang.menuitem.unnamed');
+ }
+
+ return $itemInfo['title'];
+ }
+
+ return strlen($itemInfo) ? $itemInfo : trans('rainlab.pages::lang.menuitem.unnamed');
+ }
+}
diff --git a/plugins/rainlab/pages/formwidgets/MenuPicker.php b/plugins/rainlab/pages/formwidgets/MenuPicker.php
new file mode 100644
index 000000000..5ba38f4e5
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/MenuPicker.php
@@ -0,0 +1,49 @@
+prepareVars();
+
+ return $this->makePartial('~/modules/backend/widgets/form/partials/_field_dropdown.htm');
+ }
+
+ /**
+ * Prepares the view data
+ */
+ public function prepareVars()
+ {
+ $this->vars['field'] = $this->makeFormField();
+ }
+
+ protected function makeFormField(): FormField
+ {
+ $field = clone $this->formField;
+ $field->type = 'dropdown';
+ $field->options = $this->getOptions();
+
+ return $field;
+ }
+
+ protected function getOptions(): array
+ {
+ return Menu::listInTheme(Theme::getEditTheme(), true)
+ ->mapWithKeys(function ($menu) {
+ return [
+ $menu->code => $menu->name,
+ ];
+ })->toArray();
+ }
+}
diff --git a/plugins/rainlab/pages/formwidgets/PagePicker.php b/plugins/rainlab/pages/formwidgets/PagePicker.php
new file mode 100644
index 000000000..c6c5f30d6
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/PagePicker.php
@@ -0,0 +1,64 @@
+prepareVars();
+ return $this->makePartial('~/modules/backend/widgets/form/partials/_field_dropdown.htm');
+ }
+
+ /**
+ * Prepares the view data
+ */
+ public function prepareVars()
+ {
+ $this->vars['field'] = $this->makeFormField();
+ }
+
+ /**
+ * @return \Backend\Classes\FormField
+ */
+ protected function makeFormField()
+ {
+ $field = clone $this->formField;
+ $field->type = 'dropdown';
+
+ $tree = Page::buildMenuTree(Theme::getEditTheme());
+ $indent = $field->getConfig('indent', $this->indent);
+
+ // Flatten page tree for dropdown options
+ $options = [];
+ $iterator = function($items, $depth=0) use(&$iterator, &$tree, &$options, $indent) {
+
+ foreach ($items as $code) {
+ $itemData = $tree[$code];
+ $options[$code] = str_repeat($indent, $depth) . $itemData['title'];
+ if (!empty($itemData['items'])) {
+ $iterator($itemData['items'], $depth+1);
+ }
+ }
+
+ return $options;
+ };
+
+ $field->options = $iterator($tree['--root-pages--']);
+
+ return $field;
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/pages/formwidgets/menuitems/assets/js/menu-items-editor.js b/plugins/rainlab/pages/formwidgets/menuitems/assets/js/menu-items-editor.js
new file mode 100644
index 000000000..fe750976f
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitems/assets/js/menu-items-editor.js
@@ -0,0 +1,627 @@
+/*
+ * The menu item editor. Provides tools for managing the
+ * menu items.
+ */
++function ($) { "use strict";
+ var MenuItemsEditor = function (el, options) {
+ this.$el = $(el)
+ this.options = options
+
+ this.init()
+ }
+
+ MenuItemsEditor.prototype.init = function() {
+ var self = this
+
+ this.alias = this.$el.data('alias')
+ this.$treeView = this.$el.find('div[data-control="treeview"]')
+
+ this.typeInfo = {}
+
+ // Menu items is clicked
+ this.$el.on('open.oc.treeview', function(e) {
+ return self.onItemClick(e.relatedTarget)
+ })
+
+ // Submenu item is clicked in the master tabs
+ this.$el.on('submenu.oc.treeview', $.proxy(this.onSubmenuItemClick, this))
+
+ this.$el.on('click', 'a[data-control="add-item"]', function(e) {
+ self.onCreateItem(e.target)
+ return false
+ })
+ }
+
+ /*
+ * Triggered when a submenu item is clicked in the menu editor.
+ */
+ MenuItemsEditor.prototype.onSubmenuItemClick = function(e) {
+ if ($(e.relatedTarget).data('control') == 'delete-menu-item')
+ this.onDeleteMenuItem(e.relatedTarget)
+
+ if ($(e.relatedTarget).data('control') == 'create-item')
+ this.onCreateItem(e.relatedTarget)
+
+ return false
+ }
+
+ /*
+ * Removes a menu item
+ */
+ MenuItemsEditor.prototype.onDeleteMenuItem = function(link) {
+ if (!confirm('Do you really want to delete the menu item? This will also delete the subitems, if any.'))
+ return
+
+ $(link).trigger('change')
+ $(link).closest('li[data-menu-item]').remove()
+
+ $(window).trigger('oc.updateUi')
+
+ this.$treeView.treeView('update')
+ this.$treeView.treeView('fixSubItems')
+ }
+
+ /*
+ * Opens the menu item editor
+ */
+ MenuItemsEditor.prototype.onItemClick = function(item, newItemMode) {
+ var $item = $(item),
+ $container = $('> div', $item),
+ self = this
+
+ $container.one('show.oc.popup', function(e){
+ $(document).trigger('render')
+
+ self.$popupContainer = $(e.relatedTarget);
+ self.$itemDataContainer = $container.closest('li')
+
+ $('input[type=checkbox]', self.$popupContainer).removeAttr('checked')
+
+ self.loadProperties(self.$popupContainer, self.$itemDataContainer.data('menu-item'))
+ self.$popupForm = self.$popupContainer.find('form')
+ self.itemSaved = false
+
+ var $titleField = $('input[name=title]', self.$popupContainer).focus().select()
+ var $typeField = $('select[name=type]', self.$popupContainer).change(function(){
+ self.loadTypeInfo(false, true)
+ })
+
+ $('select[name=reference]', self.$popupContainer).change(function() {
+ var selectedTitle = $(this).find('option:selected').text();
+ // If the saved title is the default new item title, use reference title,
+ // removing CMS page [base file name] suffix
+ if (selectedTitle && self.properties.title === self.$popupForm.attr('data-new-item-title')) {
+ var title = $.trim(selectedTitle.replace(/\s*\[.*\]$/, ''))
+ $titleField.val(title)
+
+ // Support for RainLab.Translate
+ var defaultLocale = $('[data-control="multilingual"]').data('default-locale')
+ if (defaultLocale) {
+ $('[name="RLTranslate['+defaultLocale+'][title]"]', self.$popupContainer).val(title)
+ }
+ }
+ })
+
+ self.$popupContainer.on('keydown', function(e) {
+ if (e.which == 13)
+ self.applyMenuItem()
+ })
+
+ $('button[data-control="apply-btn"]', self.$popupContainer).click($.proxy(self.applyMenuItem, self))
+
+ var $updateTypeOptionsBtn = $('')
+ $('div[data-field-name=reference]').addClass('input-sidebar-control').append($updateTypeOptionsBtn)
+
+ $updateTypeOptionsBtn.click(function(){
+ self.loadTypeInfo(true)
+
+ return false
+ })
+
+ $updateTypeOptionsBtn.keydown(function(ev){
+ if (ev.which == 13 || ev.which == 32) {
+ self.loadTypeInfo(true)
+ return false
+ }
+ })
+
+ self.$popupContainer.on('change', 'select[name="referenceSearch"]', function() {
+ var $select = $(this),
+ val = $select.val(),
+ parts
+
+ if (!val) return
+
+ // type::reference ID
+ parts = val.split('::', 2)
+
+ self.referenceSearchOverride = parts[1];
+
+ $select.empty().trigger('change.select2');
+
+ $typeField
+ .val(parts[0])
+ .triggerHandler('change')
+ })
+
+ var $updateCmsPagesBtn = $updateTypeOptionsBtn.clone(true)
+ $('div[data-field-name=cmsPage]').addClass('input-sidebar-control').append($updateCmsPagesBtn)
+
+ self.loadTypeInfo()
+ })
+
+ $container.one('hide.oc.popup', function(e) {
+ if (!self.itemSaved && newItemMode)
+ $item.remove()
+
+ self.$treeView.treeView('update')
+ self.$treeView.treeView('fixSubItems')
+
+ $container.removeClass('popover-highlight')
+ })
+
+ $container.popup({
+ content: $('script[data-editor-template]', this.$el).html()
+ })
+
+ /*
+ * Highlight modal target
+ */
+ $container.addClass('popover-highlight')
+ $container.blur()
+
+ return false
+ }
+
+ MenuItemsEditor.prototype.loadProperties = function($popupContainer, properties) {
+ this.properties = properties
+
+ var setPropertyOnElement = function($input, val) {
+ if ($input.prop('type') == 'checkbox') {
+ var checked = !(val == '0' || val == 'false' || val == 0 || val == undefined || val == null)
+ checked ? $input.prop('checked', 'checked') : $input.removeAttr('checked')
+ }
+ else if ($input.prop('type') == 'radio') {
+ $input.filter('[value="'+val+'"]').prop('checked', true)
+ }
+ else {
+ $input.val(val)
+ $input.change()
+ }
+ }
+
+ var defaultLocale = $('[data-control="multilingual"]').data('default-locale')
+ $.each(properties, function(property, val) {
+ if (property == 'viewBag') {
+ $.each(val, function(vbProperty, vbVal) {
+ var $input = $('[name="viewBag['+vbProperty+']"]', $popupContainer).not('[type=hidden]')
+ setPropertyOnElement($input, vbVal)
+ // Ensure that locale specific data is made available in the RainLab.Translate data holders
+ if (vbProperty === 'locale') {
+ $.each(vbVal, function(locale, fields) {
+ $.each(fields, function(fieldName, fieldValue) {
+ var $locker = $('[name="RLTranslate['+locale+']['+fieldName+']"]', $popupContainer)
+ if ($locker) {
+ $locker.val(fieldValue)
+ }
+ })
+ })
+ }
+ })
+
+ /**
+ * Mediafinder support
+ */
+ var mediafinderElements = $('[data-control="mediafinder"]');
+ var storageMediaPath = $('[data-storage-media-path]').data('storage-media-path');
+
+ $.each(mediafinderElements, function() {
+
+ var input = $(this).find('>input');
+ var propertyName = input.attr('name');
+
+ if( propertyName.length ) {
+ var propertyNameSimple = propertyName.substr(8).slice(0,-1);
+ }
+
+ var propertyValue = '';
+
+ $.each(val, function(vbProperty, vbVal) {
+ if( vbProperty == propertyNameSimple ) {
+ propertyValue = vbVal;
+ }
+ });
+
+ if( propertyValue != '' ) {
+
+ $(this).toggleClass('is-populated');
+ input.attr('value', propertyValue);
+
+ var image = $(this).find('[data-find-image]');
+
+ if( image.length ) {
+ image.attr('src', storageMediaPath + propertyValue );
+ }
+
+ var file = $(this).find('[data-find-file-name]');
+
+ if( file.length ) {
+ file.text( propertyValue.substr(1) );
+ }
+
+ }
+
+ });
+
+ }
+ else {
+ var $input = $('[name="'+property+'"]', $popupContainer).not('[type=hidden]')
+ setPropertyOnElement($input, val)
+ // If the RainLab.Translate default locale data locker fields are available make sure that they are properly populated
+ var $defaultLocaleField = $('[name="RLTranslate['+defaultLocale+']['+property+']"]', self.$popupContainer)
+ if ($defaultLocaleField) {
+ $defaultLocaleField.val($input.val());
+ }
+ }
+ })
+ }
+
+ MenuItemsEditor.prototype.loadTypeInfo = function(force, focusList) {
+ var type = $('select[name=type]', this.$popupContainer).val()
+
+ var self = this
+
+ if (!force && this.typeInfo[type] !== undefined) {
+ self.applyTypeInfo(this.typeInfo[type], type, focusList)
+ return
+ }
+
+ $.oc.stripeLoadIndicator.show()
+ this.$popupForm.request('onGetMenuItemTypeInfo')
+ .always(function(){
+ $.oc.stripeLoadIndicator.hide()
+ })
+ .done(function(data){
+ self.typeInfo[type] = data.menuItemTypeInfo
+ self.applyTypeInfo(data.menuItemTypeInfo, type, focusList)
+ })
+ }
+
+ MenuItemsEditor.prototype.applyTypeInfo = function(typeInfo, type, focusList) {
+ var $referenceFormGroup = $('div[data-field-name="reference"]', this.$popupContainer),
+ $optionSelector = $('select', $referenceFormGroup),
+ $nestingFormGroup = $('div[data-field-name="nesting"]', this.$popupContainer),
+ $urlFormGroup = $('div[data-field-name="url"]', this.$popupContainer),
+ $replaceFormGroup = $('div[data-field-name="replace"]', this.$popupContainer),
+ $cmsPageFormGroup = $('div[data-field-name="cmsPage"]', this.$popupContainer),
+ $cmsPageSelector = $('select', $cmsPageFormGroup),
+ prevSelectedReference = $optionSelector.val(),
+ prevSelectedPage = $cmsPageSelector.val()
+
+ // Search selection
+ if (this.referenceSearchOverride) {
+ prevSelectedReference = this.referenceSearchOverride;
+ this.referenceSearchOverride = null;
+ }
+
+ if (typeInfo.references) {
+ $optionSelector.find('option').remove()
+ $referenceFormGroup.show()
+
+ var iterator = function(options, level, path) {
+ $.each(options, function(code) {
+ var $option = $(' ').attr('value', code),
+ offset = Array(level*4).join(' '),
+ isObject = $.type(this) == 'object'
+
+ $option.text(isObject ? this.title : this)
+
+ var optionPath = path.length > 0
+ ? (path + ' / ' + $option.text())
+ : $option.text()
+
+ $option.data('path', optionPath)
+
+ $option.html(offset + $option.html())
+
+ $optionSelector.append($option)
+
+ if (isObject)
+ iterator(this.items, level+1, optionPath)
+ })
+ }
+
+ iterator(typeInfo.references, 0, '')
+
+ $optionSelector
+ .val(prevSelectedReference ? prevSelectedReference : this.properties.reference)
+ .triggerHandler('change')
+ }
+ else {
+ $referenceFormGroup.hide()
+ }
+
+ if (typeInfo.cmsPages) {
+ $cmsPageSelector.find('option').remove()
+ $cmsPageFormGroup.show()
+
+ $.each(typeInfo.cmsPages, function(code) {
+ var $option = $(' ').attr('value', code)
+
+ $option.text(this).val(code)
+ $cmsPageSelector.append($option)
+ })
+
+ $cmsPageSelector
+ .val(prevSelectedPage ? prevSelectedPage : this.properties.cmsPage)
+ .triggerHandler('change')
+ }
+ else {
+ $cmsPageFormGroup.hide()
+ }
+
+ $nestingFormGroup.toggle(typeInfo.nesting !== undefined && typeInfo.nesting)
+ $urlFormGroup.toggle(type == 'url')
+ $replaceFormGroup.toggle(typeInfo.dynamicItems !== undefined && typeInfo.dynamicItems)
+
+ $(document).trigger('render')
+
+ if (focusList) {
+ var focusElements = [
+ $referenceFormGroup,
+ $cmsPageFormGroup,
+ $('div.custom-checkbox', $nestingFormGroup),
+ $('div.custom-checkbox', $replaceFormGroup),
+ $('input', $urlFormGroup)
+ ]
+
+ $.each(focusElements, function(){
+ if (this.is(':visible')) {
+ var $self = this
+
+ window.setTimeout(function() {
+ if ($self.hasClass('dropdown-field'))
+ $('select', $self).select2('focus', 100)
+ else $self.focus()
+ })
+
+ return false;
+ }
+ })
+ }
+ }
+
+ MenuItemsEditor.prototype.applyMenuItem = function() {
+ var self = this,
+ data = {},
+ propertyNames = this.$el.data('item-properties'),
+ basicProperties = {
+ 'title': 1,
+ 'type': 1,
+ 'code': 1
+ },
+ typeInfoPropertyMap = {
+ reference: 'references',
+ replace: 'dynamicItems',
+ cmsPage: 'cmsPages'
+ },
+ typeInfo = {},
+ validationErrorFound = false
+
+ // Ensure that locale specific data is made available in the RainLab.Translate data holders
+ $('[name^="viewBag[locale]"]', self.$popupContainer).each(function() {
+ var locale = $(this).data('locale')
+ var fieldName = $(this).data('field-name')
+ var $localeField = $('[name="RLTranslate['+locale+']['+fieldName+']"]', self.$popupContainer)
+ $(this).val($localeField.val())
+ });
+
+ var defaultLocale = $('[data-control="multilingual"]').data('default-locale')
+
+ $.each(propertyNames, function() {
+ var propertyName = this,
+ $input = $('[name="'+propertyName+'"]', self.$popupContainer).not('[type=hidden]')
+
+ // If the RainLab.Translate default locale data locker fields are available make sure the regular inputs are properly populated
+ if (defaultLocale) {
+ var $defaultLocaleField = $('[name="RLTranslate['+defaultLocale+']['+propertyName+']"]', self.$popupContainer)
+ if ($defaultLocaleField && $defaultLocaleField.val()) {
+ $input.val($defaultLocaleField.val())
+ }
+ }
+
+ if ($input.prop('type') !== 'checkbox') {
+ data[propertyName] = $.trim($input.val())
+
+ if (propertyName == 'type')
+ typeInfo = self.typeInfo[data.type]
+
+ if (data[propertyName].length == 0) {
+ var typeInfoProperty = typeInfoPropertyMap[propertyName] !== undefined ? typeInfoPropertyMap[propertyName] : propertyName
+
+ if (typeInfo[typeInfoProperty] !== undefined) {
+
+ $.oc.flashMsg({
+ class: 'error',
+ text: self.$popupForm.attr('data-message-'+propertyName+'-required')
+ })
+
+ if ($input.prop("tagName") == 'SELECT')
+ $input.select2('focus')
+ else
+ $input.focus()
+
+ validationErrorFound = true
+
+ return false
+ }
+ }
+ }
+ else {
+ data[propertyName] = $input.prop('checked') ? 1 : 0
+ }
+ })
+
+ if (validationErrorFound)
+ return
+
+ if (data.type !== 'url') {
+ delete data['url']
+
+ $.each(data, function(property) {
+ if (property == 'type')
+ return
+
+ var typeInfoProperty = typeInfoPropertyMap[property] !== undefined ? typeInfoPropertyMap[property] : property
+ if ((typeInfo[typeInfoProperty] === undefined || typeInfo[typeInfoProperty] === false)
+ && basicProperties[property] === undefined)
+ delete data[property]
+ })
+ }
+ else {
+ $.each(propertyNames, function(){
+ if (this != 'url' && basicProperties[this] === undefined)
+ delete data[this]
+ })
+ }
+
+ if ($.trim(data.title).length == 0) {
+ $.oc.flashMsg({
+ class: 'error',
+ text: self.$popupForm.data('messageTitleRequired')
+ })
+
+ $('[name=title]', self.$popupContainer).focus()
+
+ return
+ }
+
+ if (data.type == 'url' && $.trim(data.url).length == 0) {
+ $.oc.flashMsg({
+ class: 'error',
+ text: self.$popupForm.data('messageUrlRequired')
+ })
+
+ $('[name=url]', self.$popupContainer).focus()
+
+ return
+ }
+
+ $('> div span.title', self.$itemDataContainer).text(data.title)
+
+ var referenceDescription = $.trim($('select[name=type] option:selected', self.$popupContainer).text())
+
+ if (data.type == 'url') {
+ referenceDescription += ': ' + $('input[name=url]', self.$popupContainer).val()
+ }
+ else if (typeInfo.references) {
+ referenceDescription += ': ' + $.trim($('select[name=reference] option:selected', self.$popupContainer).data('path'))
+ }
+
+ $('> div span.comment', self.$itemDataContainer).text(referenceDescription)
+
+ this.attachViewBagData(data)
+
+ this.$itemDataContainer.data('menu-item', data)
+ this.itemSaved = true
+ this.$popupContainer.trigger('close.oc.popup')
+ this.$el.trigger('change')
+ }
+
+ MenuItemsEditor.prototype.attachViewBagData = function(data) {
+ var fields = this.$popupForm.serializeArray(),
+ fieldName,
+ fieldValue
+
+ $.each(fields, function(index, field) {
+ fieldName = field.name
+ fieldValue = field.value
+
+ if (fieldName.indexOf('viewBag[') != 0) {
+ return true // Continue
+ }
+
+ /*
+ * Break field name in to elements
+ */
+ var elements = [],
+ searchResult,
+ expression = /([^\]\[]+)/g
+
+ while ((searchResult = expression.exec(fieldName))) {
+ elements.push(searchResult[0])
+ }
+
+ /*
+ * Attach elements to data with value
+ */
+ var currentData = data,
+ elementsNum = elements.length,
+ lastIndex = elementsNum - 1,
+ currentProperty
+
+ for (var i = 0; i < elementsNum; ++i) {
+ currentProperty = elements[i]
+
+ if (i === lastIndex) {
+ currentData[currentProperty] = fieldValue
+ }
+ else if (currentData[currentProperty] === undefined) {
+ currentData[currentProperty] = {}
+ }
+
+ currentData = currentData[currentProperty]
+ }
+ })
+ }
+
+ MenuItemsEditor.prototype.onCreateItem = function(target) {
+ var parentList = $(target).closest('li[data-menu-item]').find(' > ol'),
+ item = $($('script[data-item-template]', this.$el).html())
+
+ if (!parentList.length)
+ parentList = $(target).closest('div[data-control=treeview]').find(' > ol')
+
+ parentList.append(item)
+ this.$treeView.treeView('update')
+ $(window).trigger('oc.updateUi')
+
+ this.onItemClick(item, true)
+ }
+
+ MenuItemsEditor.DEFAULTS = {
+ }
+
+ // MENUITEMSEDITOR PLUGIN DEFINITION
+ // ============================
+
+ var old = $.fn.menuItemsEditor
+
+ $.fn.menuItemsEditor = function (option) {
+ var args = Array.prototype.slice.call(arguments, 1)
+ return this.each(function () {
+ var $this = $(this)
+ var data = $this.data('oc.menuitemseditor')
+ var options = $.extend({}, MenuItemsEditor.DEFAULTS, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('oc.menuitemseditor', (data = new MenuItemsEditor(this, options)))
+ else if (typeof option == 'string') data[option].apply(data, args)
+ })
+ }
+
+ $.fn.menuItemsEditor.Constructor = MenuItemsEditor
+
+ // MENUITEMSEDITOR NO CONFLICT
+ // =================
+
+ $.fn.menuItemsEditor.noConflict = function () {
+ $.fn.menuItemsEditor = old
+ return this
+ }
+
+ // MENUITEMSEDITOR DATA-API
+ // ===============
+
+ $(document).on('render', function() {
+ $('[data-control="menu-item-editor"]').menuItemsEditor()
+ });
+}(window.jQuery);
diff --git a/plugins/rainlab/pages/formwidgets/menuitems/partials/_editortemplate.htm b/plugins/rainlab/pages/formwidgets/menuitems/partials/_editortemplate.htm
new file mode 100644
index 000000000..e9105bb12
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitems/partials/_editortemplate.htm
@@ -0,0 +1,29 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/formwidgets/menuitems/partials/_item.htm b/plugins/rainlab/pages/formwidgets/menuitems/partials/_item.htm
new file mode 100644
index 000000000..2c320a859
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitems/partials/_item.htm
@@ -0,0 +1,38 @@
+
+
+
+
+
+ items): ?>
+ = $this->makePartial('itemlist', ['items' => $subItems]) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/formwidgets/menuitems/partials/_itemlist.htm b/plugins/rainlab/pages/formwidgets/menuitems/partials/_itemlist.htm
new file mode 100644
index 000000000..8a061b8e1
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitems/partials/_itemlist.htm
@@ -0,0 +1,5 @@
+
+
+ = $this->makePartial('item', ['item' => $item]) ?>
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/formwidgets/menuitems/partials/_items.htm b/plugins/rainlab/pages/formwidgets/menuitems/partials/_items.htm
new file mode 100644
index 000000000..8f1d805bf
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitems/partials/_items.htm
@@ -0,0 +1,11 @@
+
+ = $this->makePartial('itemlist', ['items' => $items]) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/formwidgets/menuitems/partials/_menuitems.htm b/plugins/rainlab/pages/formwidgets/menuitems/partials/_menuitems.htm
new file mode 100644
index 000000000..de2babcff
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitems/partials/_menuitems.htm
@@ -0,0 +1,27 @@
+
+
diff --git a/plugins/rainlab/pages/formwidgets/menuitemsearch/partials/_body.htm b/plugins/rainlab/pages/formwidgets/menuitemsearch/partials/_body.htm
new file mode 100644
index 000000000..a42399df4
--- /dev/null
+++ b/plugins/rainlab/pages/formwidgets/menuitemsearch/partials/_body.htm
@@ -0,0 +1,10 @@
+
diff --git a/plugins/rainlab/pages/lang/cs/lang.php b/plugins/rainlab/pages/lang/cs/lang.php
new file mode 100644
index 000000000..0123f4294
--- /dev/null
+++ b/plugins/rainlab/pages/lang/cs/lang.php
@@ -0,0 +1,125 @@
+ [
+ 'name' => 'Stránky',
+ 'description' => 'Funkce pro správu stránek a menu.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Stránky',
+ 'template_title' => '%s Stránky',
+ 'delete_confirmation' => 'Opravdu chcete odstranit vybrané stránky? Budou odstraněny i případné podstránky.',
+ 'no_records' => 'Stránky nenalezeny',
+ 'delete_confirm_single' => 'Opravu chcete odstranit tuto stránku? Budou odstraněny i případné podstránky.',
+ 'new' => 'Nová stránka',
+ 'add_subpage' => 'Přidat podstránku',
+ 'invalid_url' => 'Neplatný formát URL. URL by mělo začínat lomítkem a může obsahovat čísla, písmena a znaky: _-/',
+ 'url_not_unique' => 'Toto URL je používáno jinou stránkou.',
+ 'layout' => 'Layouty',
+ 'layouts_not_found' => 'Layouts nenalezeny',
+ 'saved' => 'Stránka byla úspěšně uložena.',
+ 'tab' => 'Stránky',
+ 'manage_pages' => 'Spravovat stránky',
+ 'manage_menus' => 'Spravovat menu',
+ 'access_snippets' => 'Používat snippety',
+ 'manage_content' => 'Spravovat obsah'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menu',
+ 'delete_confirmation' => 'Opravdu chcete odstranit vybraná menu?',
+ 'no_records' => 'Položky nenalezeny',
+ 'new' => 'Nové menu',
+ 'new_name' => 'Nové menu',
+ 'new_code' => 'nove-menu',
+ 'delete_confirm_single' => 'Opravdu chcete odstranit toto menu?',
+ 'saved' => 'Menu bylo úspěšně uloženo.',
+ 'name' => 'Název',
+ 'code' => 'Kód',
+ 'items' => 'Položky menu',
+ 'add_subitem' => 'Přidat položku',
+ 'code_required' => 'Pole kód je povinné.',
+ 'invalid_code' => 'Pole kód obsahuje neplatné znaky. Může obsahovat pouze číslice, písmena a znaky: _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Titulek',
+ 'editor_title' => 'Upravit položku menu',
+ 'type' => 'Typ',
+ 'allow_nested_items' => 'Povolit vnořené položky',
+ 'allow_nested_items_comment' => 'Vnořené položky mohou být automaticky vygenerovány statickými stránkami nebo jinými typy stránek.',
+ 'url' => 'URL',
+ 'reference' => 'Odkaz',
+ 'search_placeholder' => 'Prohledat odkazy...',
+ 'title_required' => 'Titulek je povinný',
+ 'unknown_type' => 'Neznámý typ položky',
+ 'unnamed' => 'Nepojmenovaný typ položky',
+ 'add_item' => 'Přidat položku',
+ 'new_item' => 'Nová položka menu',
+ 'replace' => 'Nahradit tuto položku jejími vygenerovanými vnořenými položkami',
+ 'replace_comment' => 'Toto pole zaškrtněte, pokud si přejete vnořené položky posunout na stejnou úroveň jako má tato položka. Samotná položka zůstane skryta.',
+ 'cms_page' => 'CMS stránka',
+ 'cms_page_comment' => 'Vyberte stránku, která se otevře při kliknutí na tuto položku v menu.',
+ 'reference_required' => 'Odkaz je povinný.',
+ 'url_required' => 'URL je povinné',
+ 'cms_page_required' => 'Prosím vyberte CMS stránku',
+ 'display_tab' => 'Zobrazení',
+ 'hidden' => 'Skrytá',
+ 'hidden_comment' => 'Skrýt tuto položku menu pro celý front-end.',
+ 'attributes_tab' => 'Vlastnosti',
+ 'code' => 'Kód',
+ 'code_comment' => 'Zadejte kód položky menu pokud k ní chcete přistupovat přes API.',
+ 'css_class' => 'CSS třída',
+ 'css_class_comment' => 'Vložte název CSS třídy k zajištění specifického vzhledu této položky menu.',
+ 'external_link' => 'Externí odkaz',
+ 'external_link_comment' => 'Otevřít odkaz této položky v novém okně.',
+ 'static_page' => 'Statická stránka',
+ 'all_static_pages' => 'Všechny statické stránky'
+ ],
+ 'content' => [
+ 'menu_label' => 'Obsah',
+ 'cant_save_to_dir' => 'Ukládat obsah do složky statických stránek není povoleno.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Přidat',
+ 'search' => 'Hledat...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Neznámý typ objektu',
+ 'not_found' => 'Požadovaný objekt nebyl nalezen.'
+ ],
+ 'editor' => [
+ 'title' => 'Titulek',
+ 'new_title' => 'Nový titulek stránky',
+ 'content' => 'Obsah',
+ 'url' => 'URL',
+ 'filename' => 'Název souboru',
+ 'layout' => 'Layout',
+ 'description' => 'Popis',
+ 'preview' => 'Náhled',
+ 'enter_fullscreen' => 'Vstoupit do režimu celé obrazovky',
+ 'exit_fullscreen' => 'Opustit režim celé obrazovky',
+ 'hidden' => 'Skrytá',
+ 'hidden_comment' => 'Skryté stránky jsou dostupné pouze přihlášeným administrátorům.',
+ 'navigation_hidden' => 'Skrýt v menu',
+ 'navigation_hidden_comment' => 'Zaškrtněte toto pole, pokud chcete stránku skrýt z automaticky vygenerovaných menu a drobečkové navigace.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Kód snippetu',
+ 'code_comment' => 'Zadejte kód, aby tato dílčí šablona byla dostupná jako snippet v pluginu statických stránek.',
+ 'name' => 'Název',
+ 'name_comment' => 'Tento název bude zobrazen v seznamu snippetů v bočním menu pluginu statických stránek a přímo na stránce, když bude snippet přidán.',
+ 'no_records' => 'Snippety nenalezeny',
+ 'menu_label' => 'Snippety',
+ 'column_property' => 'Název vlastnosti',
+ 'column_type' => 'Vlastnosti',
+ 'column_code' => 'Kód',
+ 'column_default' => 'Výchozí',
+ 'column_options' => 'Možnosti',
+ 'column_type_string' => 'Řetězec',
+ 'column_type_checkbox' => 'Zaškrtávací políčko',
+ 'column_type_dropdown' => 'Seznam',
+ 'not_found' => 'Snippet s kódem :code nebyl nalezen v rámci tématu.',
+ 'property_format_error' => 'Kód vlastnosti by měl začínat písmenem a může obsahovat pouze písmena nebo číslice.',
+ 'invalid_option_key' => 'Neplatný klíč položky seznamu: %s. Klíč položky seznamu může obsahovat pouze písmena, číslice a znaky: _-'
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/de/lang.php b/plugins/rainlab/pages/lang/de/lang.php
new file mode 100644
index 000000000..3a99b35a9
--- /dev/null
+++ b/plugins/rainlab/pages/lang/de/lang.php
@@ -0,0 +1,114 @@
+ [
+ 'name' => 'Seiten',
+ 'description' => 'Pages & menus features.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Seiten',
+ 'template_title' => '%s Seiten',
+ 'delete_confirmation' => 'Möchten Sie die ausgewählten Seiten wirklich löschen? Dadurch werden auch mögliche Unterseiten gelöscht.',
+ 'no_records' => 'Keine Seiten gefunden',
+ 'delete_confirm_single' => 'Möchten Sie die ausgewählte Seite wirklich löschen? Dadurch werden auch mögliche Unterseiten gelöscht.',
+ 'new' => 'Neue Seite',
+ 'add_subpage' => 'Neue Unterseite',
+ 'invalid_url' => 'Ungültiges URL Format. Die URL sollte mit einem Schrägstrich (Slash) starten und darf Ziffern, Buchstaben und folgenden Symbole enthalten: _-/.',
+ 'url_not_unique' => 'Die URL wird schon von einer anderen Seite benutzt.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Keine Layouts gefunden',
+ 'saved' => 'Die Seite wurde erfolgreich gespeichert.',
+ 'manage_pages' => 'Verwalte statische Seiten',
+ 'manage_menus' => 'Verwalte statische Menüs',
+ 'access_snippets' => 'Verwalte Snippets',
+ 'manage_content' => 'Verwalte den Inhalt'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menüs',
+ 'delete_confirmation' => 'Möchten Sie die ausgewählten Menüs wirklich löschen?',
+ 'no_records' => 'Keine Menüs gefunden',
+ 'new' => 'Neues Menü',
+ 'new_name' => 'Menüname',
+ 'new_code' => 'menuename',
+ 'delete_confirm_single' => 'Möchten Sie das ausgewählte Menü wirklich löschen?',
+ 'saved' => 'Das Menü wurde erfolgreich gespeichert.',
+ 'name' => 'Name',
+ 'code' => 'Code',
+ 'items' => 'Menüpunkte',
+ 'add_subitem' => 'Neuer Menüpunkt',
+ 'no_records' => 'Keine Menüpunkte gefunden',
+ 'code_required' => 'Ein Code ist erforderlich',
+ 'invalid_code' => 'Ungültiges Code Format. Der Code darf Ziffern, Buchstaben und folgenden Symbole enthalten: _-/'
+ ],
+ 'menuitem' => [
+ 'title' => 'Titel',
+ 'editor_title' => 'Menüpunkt bearbeiten',
+ 'type' => 'Typ',
+ 'allow_nested_items' => 'Erlaube verschachtelte Menüpunkte',
+ 'allow_nested_items_comment' => 'Verschachtelte Menüpunkte können dynamisch durch statische Seiten und einigen anderen Menüpunkt-Typen erzeugt werden.',
+ 'url' => 'URL',
+ 'reference' => 'Referenz',
+ 'title_required' => 'Ein Titel ist erforderlich',
+ 'unknown_type' => 'Unbekannter Menüpunkt-Typ',
+ 'unnamed' => 'Unbekannter Menüpunkt',
+ 'add_item' => 'Neuer Menüpunkt',
+ 'new_item' => 'Neuer Menüpunkt',
+ 'replace' => 'Ersetze diesen Menüpunkt mit seinen Unterpunkten',
+ 'replace_comment' => 'Verwenden Sie diese Option, um erzeugte Menüpunkte auf die gleiche Ebene von diesem zu bringen. Dieser Menüpunkt selbst wird ausgeblendet.',
+ 'cms_page' => 'CMS Seite',
+ 'cms_page_comment' => 'Wählen Sie eine Seite die geöffnet werden soll, wenn dieser Menüpunkt angeklickt wird.',
+ 'reference_required' => 'Eine Menüpunkt-Referenz ist erforderlich',
+ 'url_required' => 'Eine URL ist erforderlich',
+ 'cms_page_required' => 'Bitten wählen Sie eine CMS Seite',
+ 'code' => 'Code',
+ 'code_comment' => 'Geben Sie einen Menüpunkt-Code ein, wenn Sie diesen mit der API ansprechen möchten.'
+ ],
+ 'content' => [
+ 'menu_label' => 'Inhalte',
+ 'cant_save_to_dir' => 'Das Speichern von Inhaltsdateien in den Statische-Seiten Ordner ist nicht erlaubt.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Neu',
+ 'search' => 'Suche...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Unbekannter Objekttyp',
+ 'not_found' => 'Das angeforderte Objekt wurde nicht gefunden.'
+ ],
+ 'editor' => [
+ 'title' => 'Titel',
+ 'new_title' => 'Titel für die neue Seite',
+ 'content' => 'Inhalt',
+ 'url' => 'URL',
+ 'filename' => 'Dateiname',
+ 'layout' => 'Layout',
+ 'description' => 'Beschreibung',
+ 'preview' => 'Vorschau',
+ 'enter_fullscreen' => 'Vollbildmodus einschalten',
+ 'exit_fullscreen' => 'Vollbildmodus verlassen',
+ 'hidden' => 'Verstecken',
+ 'hidden_comment' => 'Versteckte Seiten sind nur für eingeloggte administrations Benutzer zugänglich.',
+ 'navigation_hidden' => 'In der Navigation verstecken',
+ 'navigation_hidden_comment' => 'Setzen Sie diese Option, um diese Seite von automatisch generierten Menüs und Breadcrumbs zu verstecken.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Snippet code',
+ 'code_comment' => 'Geben Sie einen Code ein, um dieses Partial als ein Snippet für das Statische-Seiten Plugin freizugeben.',
+ 'name' => 'Name',
+ 'name_comment' => 'Der Name wird in der Snippet-Liste in der Seitenleiste der Statische-Seiten angezeigt. Außerdem auf einer Seite wenn ein Snippet angelegt wird.',
+ 'no_records' => 'Keine Snippets gefunden',
+ 'menu_label' => 'Snippets',
+ 'column_property' => 'Titel für die Eigenschaft',
+ 'column_type' => 'Typ',
+ 'column_code' => 'Code',
+ 'column_default' => 'Standard',
+ 'column_options' => 'Optionen',
+ 'column_type_string' => 'Zeichenkette',
+ 'column_type_checkbox' => 'Checkbox',
+ 'column_type_dropdown' => 'Dropdown',
+ 'not_found' => 'Das Snippet mit dem angeforderten code :code wurde nicht im Theme gefunden.',
+ 'property_format_error' => 'Der Code für die Eigenschaft muss mit einem Buchstaben anfangen, und darf nur Buchstaben und Zahlen enthalten',
+ 'invalid_option_key' => 'Ungültiger Dropdown Optionsschlüssel: %s. Optionsschlüssel dürfen nur Zahlen, Buchstaben und die Zeichen _ und - enthalten'
+ ]
+ ];
diff --git a/plugins/rainlab/pages/lang/el/lang.php b/plugins/rainlab/pages/lang/el/lang.php
new file mode 100644
index 000000000..724ce888f
--- /dev/null
+++ b/plugins/rainlab/pages/lang/el/lang.php
@@ -0,0 +1,133 @@
+ [
+ 'name' => 'Σελίδες',
+ 'description' => 'Σελίδες και Μενού.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Σελίδες',
+ 'template_title' => '%s Σελίδες',
+ 'delete_confirmation' => 'Θέλετε πραγματικά να διαγράψετε τις επιλεγμένες σελίδες; Αυτό θα διαγράψει και τις υποσελίδες, εάν υπάρχουν.',
+ 'no_records' => 'Δεν βρέθηκαν σελίδες',
+ 'delete_confirm_single' => 'Θέλετε πραγματικά να διαγράψετε αυτή την σελίδα; Αυτό θα διαγράψει και τις υποσελίδες, εάν υπάρχουν.',
+ 'new' => 'Νέα σελίδα',
+ 'add_subpage' => 'Προσθήκη υποσελίδας',
+ 'invalid_url' => 'Μή έγκυρη μορφή URL. Το URL πρέπει να αρχίζει με το σύμβολο μπροστινής καθέτου και μπορεί να περιέχει αριθμητικά ψηφία, Λατινικούς χαρακτήρες και τα ακόλουθα σύμβολα: _-/.',
+ 'url_not_unique' => 'Αυτό το URL χρησιμοποιήται ήδη από άλλη σελίδα.',
+ 'layout' => 'Σχέδιο',
+ 'layouts_not_found' => 'Δεν βρέθηκαν σχέδια',
+ 'saved' => 'Η σελίδα αποθηκεύτηκε επιτυχώς.',
+ 'tab' => 'Σελίδες',
+ 'manage_pages' => 'Διαχείριση στατικών σελίδων',
+ 'manage_menus' => 'Διαχείριση στατικών μενού',
+ 'access_snippets' => 'Πρόσβαση στα αποσπάσματα',
+ 'manage_content' => 'Διαχείριση στατικού περιεχομένου',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Μενού',
+ 'delete_confirmation' => 'Θέλετε πραγματικά να διαγράψετε τα επιλεγμένα μενού;',
+ 'no_records' => 'Δεν βρέθηκανε μενού',
+ 'new' => 'Νέο μενού',
+ 'new_name' => 'Νέο μενού',
+ 'new_code' => 'new-menu',
+ 'delete_confirm_single' => 'Θέλετε πραγματικά να διαγράψετε αυτό το μενού;',
+ 'saved' => 'Αυτό το μενού αποθηκεύτηκε επιτυχώς.',
+ 'name' => 'Όνομα',
+ 'code' => 'Κωδικός',
+ 'items' => 'Στοιχεία μενού',
+ 'add_subitem' => 'Προσθήκη υποστοιχείου',
+ 'code_required' => 'Ο Κωδικός είναι απαραίτητος',
+ 'invalid_code' => 'Μή έγκυρη μορφή Κωδικού. Ο Κωδικός μπορεί να περιέχει αριθμητικά ψηφία, Λατινικούς χαρακτήρες και τα ακόλουθα σύμβολα: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Τίτλος',
+ 'editor_title' => 'Επεξεργασία στοιχείου μενού',
+ 'type' => 'Τύπος',
+ 'allow_nested_items' => 'Να επιτρέπονται ένθετα στοιχεία',
+ 'allow_nested_items_comment' => 'Τα ένθετα στοιχεία μπορούν να δημιουργηθούν δυναμικά από στατικές σελίδες και κάποιους άλλους τύπους στοιχείων',
+ 'url' => 'URL',
+ 'reference' => 'Αναφορά',
+ 'search_placeholder' => 'Αναζήτηση αναφορών...',
+ 'title_required' => 'Ο Τίτλος είναι απαραίτητος',
+ 'unknown_type' => 'Άγνωστος τύπος στοιχείου μενού',
+ 'unnamed' => 'Στοιχείο μενού χωρίς όνομα',
+ 'add_item' => 'Προσθήκη Στοιχείου',
+ 'new_item' => 'Νέο στοιχείο μενού',
+ 'replace' => 'Αντικατάσταση του στοιχείου με τα παραγμένα υποστοιχεία',
+ 'replace_comment' => 'Χρησιμοποιήστε αυτό το πλαίσιο για να ωθήσετε τα παραγμένα στοιχεία μενού στο ίδιο επίπεδο με αυτό το στοιχείο. Το ίδιο το στοιχείο θα γίνει κρυφό.',
+ 'cms_page' => 'Σελίδα CMS',
+ 'cms_page_comment' => 'Επιλέξτε σελίδα που θα ανοίγει όταν αυτό το μενού πατηθεί.',
+ 'reference_required' => 'Η αναφορά του στοιχείου μενού είναι απαραίτητη.',
+ 'url_required' => 'Το URL είναι απαραίτητο',
+ 'cms_page_required' => 'Παρακαλώ διαλέξτε μία σελίδα CMS',
+ 'code' => 'Κωδικός',
+ 'code_comment' => 'Εισάγετε το κωδικό του μενού εάν θέλετε να έχετε πρόσβαση μέσω του API.',
+ 'static_page' => 'Στατική σελίδα',
+ 'all_static_pages' => 'Όλες οι Στατικές Σελίδες'
+ ],
+ 'content' => [
+ 'menu_label' => 'Περιεχόμενο',
+ 'cant_save_to_dir' => 'Η αποθήκευση των αρχείων περιεχομένου στο φάκελο στατικών σελίδων δεν επιτρέπεται.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Προσθήκη',
+ 'search' => 'Αναζήτηση...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Άγνωστος τύπος αντικειμένου',
+ 'not_found' => 'Το ζητούμενο αντικείμενο δεν βρέθηκε.',
+ ],
+ 'editor' => [
+ 'title' => 'Τίτλος',
+ 'new_title' => 'Νέος τίτλος σελίδας',
+ 'content' => 'Περιεχόμενο',
+ 'url' => 'URL',
+ 'filename' => 'Όνομα αρχείου',
+ 'layout' => 'Σχέδιο',
+ 'description' => 'Περιγραφή',
+ 'preview' => 'Προεπισκόπηση',
+ 'enter_fullscreen' => 'Πλήρης οθόνη',
+ 'exit_fullscreen' => 'Έξοδος πλήρους οθόνης',
+ 'hidden' => 'Κρυφό',
+ 'hidden_comment' => 'Κρυφές σελίδες είναι προσβάσιμες μόνο στους συνδεδεμένους χρήστες back-end.',
+ 'navigation_hidden' => 'Απόκρυψη στην πλοήγηση',
+ 'navigation_hidden_comment' => 'Επιλέξτε αυτό το πλαίσιο για να κρύψετε αυτή την σελίδα από αυτοματοποιημένα μενού και breadcrumbs.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Αποσπάσματα',
+ 'code' => 'Κώδικας αποσπάσματος',
+ 'code_comment' => 'Εισάγετε ένα κώδικα για να κάνεται αυτό το partial διαθέσιμο σαν απόσπασμα στο πρόσθετο Στατικών Σελίδων.',
+ 'name' => 'Όνομα',
+ 'name_comment' => 'Το όνομα εμφανίζεται στην λίστα αποσπασμάτων στην πλευρική στήλη Στατικών Σελίδων και σε μία Σελίδα όταν προστίθεται ένα απόσπασμα.',
+ 'no_records' => 'Δεν βρέθηκαν αποσπάσματα',
+ 'menu_label' => 'Αποσπάσματα',
+ 'column_property' => 'Τίτλος ιδιότητας',
+ 'column_type' => 'Τύπος',
+ 'column_code' => 'Κωδικός',
+ 'column_default' => 'Προκαθορισμένο',
+ 'column_options' => 'Επιλογές',
+ 'column_type_string' => 'Κείμενο',
+ 'column_type_checkbox' => 'Πλαίσιο ελέγχου',
+ 'column_type_dropdown' => 'Πτυσσόμενες επιλογές',
+ 'not_found' => 'Απόσπασμα με τον ζητούμενο κωδικό :code δεν βρέθηκε στο πρότυπο θέμα.',
+ 'property_format_error' => 'Οι κωδικοί των ιδιοτήτων πρέπει να ξεκινούν με Λατινικά γράμματα και μπορούν να περιέχουν μόνο Λατινικά γράμματα και αριθμητικά ψηφία',
+ 'invalid_option_key' => 'Μή έγκυρο κλειδί πτυσσόμενης επιλογής: :key. Τα κλειδιά επιλογών μπορούνα να περιέχουν μόνο αριθμιτικά ψηφία, Λατινικά γράμματα και τους χαρακτήρες _ και -',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Στατική σελίδα',
+ 'static_page_description' => 'Παράγει μία στατική σελίδα ενσωματωμένη σε ένα σχέδιο CMS.',
+ 'static_page_use_content_name' => 'Χρησιμοποιήστε το πεδίο περιεχομένου σελίδας',
+ 'static_page_use_content_description' => 'Εάν δεν είναι επιλεγμένο, η περιοχή περιεχομένου δεν θα εμφανίζεται όταν επεξεργάζεται η στατική σελίδα. Το περιεχόμενο της σελίδας θα ορίζεται αποκλειστικά μέσω αντικαταστατών και μεταβλητών.',
+ 'static_page_default_name' => 'Προεπιλεγμένο σχέδιο',
+ 'static_page_default_description' => 'Ορίζει αυτό το σχέδιο σαν το προεπιλεγμένο για καινούργιες σελίδες',
+ 'static_page_child_layout_name' => 'Σχέδιο υποσελίδων',
+ 'static_page_child_layout_description' => 'Το προεπιλεγμένο σχέδιο για καινούργιες υποσελίδες',
+ 'static_menu_name' => 'Στατικό μενού',
+ 'static_menu_description' => 'Παράγει ένα μενού σε ένα σχέδιο CMS.',
+ 'static_menu_code_name' => 'Μενού',
+ 'static_menu_code_description' => 'Ορίστε ένα κωδικό του μενού που το Δομικό Στοιχείο θα πρέπει να εξάγει.',
+ 'static_breadcrumbs_name' => 'Στατικά breadcrumbs',
+ 'static_breadcrumbs_description' => 'Παράγει breadcrumbs για μία στατική σελίδα.',
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/en/lang.php b/plugins/rainlab/pages/lang/en/lang.php
new file mode 100644
index 000000000..517baa80a
--- /dev/null
+++ b/plugins/rainlab/pages/lang/en/lang.php
@@ -0,0 +1,153 @@
+ [
+ 'name' => 'Pages',
+ 'description' => 'Pages & menus features.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Pages',
+ 'template_title' => '%s Pages',
+ 'delete_confirmation' => 'Do you really want to delete selected pages? This will also delete the subpages, if any.',
+ 'no_records' => 'No pages found',
+ 'delete_confirm_single' => 'Do you really want delete this page? This will also delete the subpages, if any.',
+ 'new' => 'New page',
+ 'add_subpage' => 'Add subpage',
+ 'invalid_url' => 'Invalid URL format. The URL should start with the forward slash symbol and can contain digits, Latin letters and the following symbols: _-/.',
+ 'url_not_unique' => 'This URL is already used by another page.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Layouts not found',
+ 'saved' => 'The page has been successfully saved.',
+ 'tab' => 'Pages',
+ 'manage_pages' => 'Manage static pages',
+ 'manage_menus' => 'Manage static menus',
+ 'access_snippets' => 'Access snippets',
+ 'manage_content' => 'Manage static content',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menus',
+ 'delete_confirmation' => 'Do you really want to delete selected menus?',
+ 'no_records' => 'No menus found',
+ 'new' => 'New menu',
+ 'new_name' => 'New menu',
+ 'new_code' => 'new-menu',
+ 'delete_confirm_single' => 'Do you really want delete this menu?',
+ 'saved' => 'The menu has been successfully saved.',
+ 'name' => 'Name',
+ 'code' => 'Code',
+ 'items' => 'Menu items',
+ 'add_subitem' => 'Add subitem',
+ 'code_required' => 'The Code is required',
+ 'invalid_code' => 'Invalid Code format. The Code can contain digits, Latin letters and the following symbols: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Title',
+ 'editor_title' => 'Edit Menu Item',
+ 'type' => 'Type',
+ 'allow_nested_items' => 'Allow nested items',
+ 'allow_nested_items_comment' => 'Nested items could be generated dynamically by static page and some other item types',
+ 'url' => 'URL',
+ 'reference' => 'Reference',
+ 'search_placeholder' => 'Search all references...',
+ 'title_required' => 'The Title is required',
+ 'unknown_type' => 'Unknown menu item type',
+ 'unnamed' => 'Unnamed menu item',
+ 'add_item' => 'Add I tem',
+ 'new_item' => 'New menu item',
+ 'replace' => 'Replace this item with its generated children',
+ 'replace_comment' => 'Use this checkbox to push generated menu items to the same level with this item. This item itself will be hidden.',
+ 'cms_page' => 'CMS Page',
+ 'cms_page_comment' => 'Select a page to open when the menu item is clicked.',
+ 'reference_required' => 'The menu item reference is required.',
+ 'url_required' => 'The URL is required',
+ 'cms_page_required' => 'Please select a CMS page',
+ 'display_tab' => 'Display',
+ 'hidden' => 'Hidden',
+ 'hidden_comment' => 'Hide this menu item from appearing on the front-end.',
+ 'attributes_tab' => 'Attributes',
+ 'code' => 'Code',
+ 'code_comment' => 'Enter the menu item code if you want to access it with the API.',
+ 'css_class' => 'CSS Class',
+ 'css_class_comment' => 'Enter a CSS class name to give this menu item a custom appearance.',
+ 'external_link' => 'External link',
+ 'external_link_comment' => 'Open links for this menu item in a new window.',
+ 'static_page' => 'Static Page',
+ 'all_static_pages' => 'All Static Pages'
+ ],
+ 'content' => [
+ 'menu_label' => 'Content',
+ 'saved' => 'The content has been successfully saved.',
+ 'cant_save_to_dir' => 'Saving content files to the static-pages directory is not allowed.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Add',
+ 'search' => 'Search...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Unknown object type',
+ 'unauthorized_type' => 'You are not authorized to manage :type objects',
+ 'not_found' => 'The requested object was not found.',
+ ],
+ 'editor' => [
+ 'title' => 'Title',
+ 'new_title' => 'New page title',
+ 'content' => 'Content',
+ 'url' => 'URL',
+ 'filename' => 'File Name',
+ 'layout' => 'Layout',
+ 'description' => 'Description',
+ 'preview' => 'Preview',
+ 'enter_fullscreen' => 'Enter fullscreen mode',
+ 'exit_fullscreen' => 'Exit fullscreen mode',
+ 'hidden' => 'Hidden',
+ 'hidden_comment' => 'Hidden pages are accessible only by logged-in back-end users.',
+ 'navigation_hidden' => 'Hide in navigation',
+ 'navigation_hidden_comment' => 'Check this box to hide this page from automatically generated menus and breadcrumbs.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'settings_popup_title' => 'Static Pages Snippet',
+ 'code' => 'Snippet code',
+ 'code_comment' => 'Enter a code to make this partial available as a snippet in the Static Pages plugin.',
+ 'code_required' => 'Please enter tne snippet code',
+ 'name' => 'Name',
+ 'name_comment' => 'The name is displayed in the snippet list in the Static Pages sidebar and on a Page when a snippet is added.',
+ 'name_required' => 'Please enter tne snippet name',
+ 'no_records' => 'No snippets found',
+ 'menu_label' => 'Snippets',
+ 'properties' => 'Snippet properties',
+ 'column_property' => 'Property Title',
+ 'title_required' => 'Please provide the property title',
+ 'type_required' => 'Please select the property type',
+ 'property_required' => 'Please provide the property name',
+ 'column_type' => 'Type',
+ 'column_type_placeholder' => 'Select',
+ 'column_code' => 'Code',
+ 'column_default' => 'Default',
+ 'column_options' => 'Options',
+ 'column_type_string' => 'String',
+ 'column_type_checkbox' => 'Checkbox',
+ 'column_type_dropdown' => 'Dropdown',
+ 'not_found' => 'Snippet with the requested code :code was not found in the theme.',
+ 'property_format_error' => 'Property code should start with a Latin letter and can contain only Latin letters and digits',
+ 'invalid_option_key' => 'Invalid drop-down option key: :key. Option keys can contain only digits, Latin letters and characters _ and -',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Static page',
+ 'static_page_description' => 'Outputs a static page in a CMS layout.',
+ 'static_page_use_content_name' => 'Use page content field',
+ 'static_page_use_content_description' => 'If unchecked, the content section will not appear when editing the static page. Page content will be determined solely through placeholders and variables.',
+ 'static_page_default_name' => 'Default layout',
+ 'static_page_default_description' => 'Defines this layout as the default for new pages',
+ 'static_page_child_layout_name' => 'Subpage layout',
+ 'static_page_child_layout_description' => 'The layout to use as the default for any new subpages',
+ 'static_menu_name' => 'Static menu',
+ 'static_menu_description' => 'Outputs a menu in a CMS layout.',
+ 'static_menu_code_name' => 'Menu',
+ 'static_menu_code_description' => 'Specify a code of the menu the component should output.',
+ 'static_breadcrumbs_name' => 'Static breadcrumbs',
+ 'static_breadcrumbs_description' => 'Outputs breadcrumbs for a static page.',
+ 'child_pages_name' => 'Child pages',
+ 'child_pages_description' => 'Displays a list of child pages for the current page',
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/es/lang.php b/plugins/rainlab/pages/lang/es/lang.php
new file mode 100644
index 000000000..1cf8dcc95
--- /dev/null
+++ b/plugins/rainlab/pages/lang/es/lang.php
@@ -0,0 +1,115 @@
+ [
+ 'name' => 'Páginas',
+ 'description' => 'Páginas & menus',
+ ],
+ 'page' => [
+ 'menu_label' => 'Páginas',
+ 'template_title' => '%s Páginas',
+ 'delete_confirmation' => 'Estas seguro de querer borrar las páginas seleccionadas? Esto también borrará las sub-páginas que existan.',
+ 'no_records' => 'No se ha encontrado ninguna página',
+ 'delete_confirm_single' => 'Estas seguro de querer borrar esta página? Esto también borrará las sub-páginas que existan.',
+ 'new' => 'Nueva página',
+ 'add_subpage' => 'Añadir sub-página',
+ 'invalid_url' => 'Formato de URL no válido. La URL debería comenzar por una barra (\'/\'). Puede contener letras, números, y los siguientes símbolos _ - / ',
+ 'url_not_unique' => 'Esta URL ya está siendo utilizada por otra página.',
+ 'layout' => 'Plantilla',
+ 'layouts_not_found' => 'No se han encontrado plantillas',
+ 'saved' => 'La página se ha guardado correctamente.',
+ 'tab' => 'Páginas',
+ 'manage_pages' => 'Administrar páginas',
+ 'manage_menus' => 'Administrar menús',
+ 'access_snippets' => 'Acceder a fragmentos',
+ 'manage_content' => 'Administrar contenidos'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menus',
+ 'delete_confirmation' => 'Estas seguro de querer borrar los menus seleccionados?',
+ 'no_records' => 'No se han encontrado menus.',
+ 'new' => 'Nuevo menu',
+ 'new_name' => 'Nuevo menu',
+ 'new_code' => 'nuevo-menu',
+ 'delete_confirm_single' => 'Estas seguro de querer borrar este menu?',
+ 'saved' => 'El menú se ha guardado correctamente.',
+ 'name' => 'Nombre',
+ 'code' => 'Código',
+ 'items' => 'Elementos del menu',
+ 'add_subitem' => 'Añadir sub-elemento',
+ 'no_records' => 'No se han encontrado elementos.',
+ 'code_required' => 'El código es obligatorio',
+ 'invalid_code' => 'El formato del código no es válido. Puede contener letras, números y los siguientes símbolos: _ - '
+ ],
+ 'menuitem' => [
+ 'title' => 'Título',
+ 'editor_title' => 'Editar elemento del menu',
+ 'type' => 'Tipo',
+ 'allow_nested_items' => 'Permitir elementos anidados',
+ 'allow_nested_items_comment' => 'Los elementos anidados se pueden generar automáticamente mediante las páginas y otros tipos de elementos.',
+ 'url' => 'URL',
+ 'reference' => 'Referencia',
+ 'title_required' => 'El título es obligatorio',
+ 'unknown_type' => 'Este tipo de elemento del menú es desconocido.',
+ 'unnamed' => 'Elemento del menú sin nombre',
+ 'add_item' => 'Añadir elemento',
+ 'new_item' => 'Nuevo elemento',
+ 'replace' => 'Sustituye este elemento por los sub-elementos que contenga.',
+ 'replace_comment' => 'Marca esta casilla sustituir este elemento por los sub-elementos que contenga. El elemento proncipal quedará oculto.',
+ 'cms_page' => 'Página del CMS',
+ 'cms_page_comment' => 'Selecciona una página a la que enlazar cuando se haga click en este elemento del menu.',
+ 'reference_required' => 'La referencia al elemento del menú es obligatoria.',
+ 'url_required' => 'La URL es obligatoria',
+ 'cms_page_required' => 'Selecciona una página del CMS',
+ 'code' => 'Código',
+ 'code_comment' => 'Introduce el código del elemento para acceder mediante la API.'
+ ],
+ 'content' => [
+ 'menu_label' => 'Contenido',
+ 'cant_save_to_dir' => 'No está permitido guardar archivos de contenido en el directorio de las páginas.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Añadir',
+ 'search' => 'Buscar...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Tipo de objeto desconocido',
+ 'not_found' => 'No se ha encontrado el objeto solicitado.'
+ ],
+ 'editor' => [
+ 'title' => 'Título',
+ 'new_title' => 'Título de la nueva página',
+ 'content' => 'Contenido',
+ 'url' => 'URL',
+ 'filename' => 'Nombre de archivo',
+ 'layout' => 'Plantilla',
+ 'description' => 'Descripción',
+ 'preview' => 'Vista previa',
+ 'enter_fullscreen' => 'Entrar en modo de pantalla completa',
+ 'exit_fullscreen' => 'Salir del modo de pantalla completa',
+ 'hidden' => 'Oculto',
+ 'hidden_comment' => 'Las páginas ocultas solo son visibles para los administradores que hayan iniciado sesión.',
+ 'navigation_hidden' => 'No mostrar en el menu',
+ 'navigation_hidden_comment' => 'Marca esta casilla para ocultar esta página en los menus generados automáticamente.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Fragmentos',
+ 'code' => 'Código del fragmento',
+ 'code_comment' => 'Introduce un código para hacer que el fragmento esté disponible en el plugin de páginas.',
+ 'name' => 'Nombre',
+ 'name_comment' => 'El nombre se muestra en la lista de fragmentos, en el menu lateral del plugin de las páginas, y en cada página en la que se haya utilizado el fragmento.',
+ 'no_records' => 'No se han encontrado fragmentos',
+ 'menu_label' => 'Fragmentos',
+ 'column_property' => 'Título de propiedad',
+ 'column_type' => 'Tipo',
+ 'column_code' => 'Código',
+ 'column_default' => 'Valor Predeterminado',
+ 'column_options' => 'Opciones',
+ 'column_type_string' => 'Texto',
+ 'column_type_checkbox' => 'Casilla',
+ 'column_type_dropdown' => 'Desplegable',
+ 'not_found' => 'No se ha encontrado ningún fragmento con el código :code en este tema.',
+ 'property_format_error' => 'El código de propiedad debería comenzar por una letra. Sólo puede contener letras y números.',
+ 'invalid_option_key' => 'La clave de opción del desplegable no es válida : %s. Estas claves sólo pueden contener números, letras y los símbolos _ y -'
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/fa/lang.php b/plugins/rainlab/pages/lang/fa/lang.php
new file mode 100644
index 000000000..22ed04590
--- /dev/null
+++ b/plugins/rainlab/pages/lang/fa/lang.php
@@ -0,0 +1,131 @@
+ [
+ 'name' => 'صفحات',
+ 'description' => 'مدیریت صفحات و فهرست ها',
+ ],
+ 'page' => [
+ 'menu_label' => 'صفحات',
+ 'template_title' => 'صفحات %s',
+ 'delete_confirmation' => 'آیا از حذف صفحات انتخاب شده اطمینان دارید؟ اگر صفحات دارای زیر صفحه باشند آنها نیز حذف خواهند شد.',
+ 'no_records' => 'صفحه ای یافت نشد',
+ 'delete_confirm_single' => 'آیا از حذف این صفحه اطمینان دارید؟ اگر این صفحه دارای زیر مجموعه باشد آنها نیز حذف خواهند شد.',
+ 'new' => 'صفحه ی جدید',
+ 'add_subpage' => 'افزودن زیر مجموعه',
+ 'invalid_url' => 'قالب آدرس نا معتبر است. آدرس باید با اسلش شروع شود و میتواند شامل حروف لاتین، حروف فارسی، اعداد و این کاراکتر ها باشد: _-/.',
+ 'url_not_unique' => 'این آدرس توسط صفحه ی دیگری استفاده شده است.',
+ 'layout' => 'طرح بندی',
+ 'layouts_not_found' => 'طرح بندی برای صفحات استاتیک یافت نشد.',
+ 'saved' => 'صفحه با موفقیت ذخیره شد.',
+ 'tab' => 'صفحات',
+ 'manage_pages' => 'مدیریت صفحات استاتیک',
+ 'manage_menus' => 'مدیریت فهرست های استاتیک',
+ 'access_snippets' => 'دسترسی به تکه کد ها',
+ 'manage_content' => 'مدیریت محتوی استاتیک',
+ ],
+ 'menu' => [
+ 'menu_label' => 'فهرست ها',
+ 'delete_confirmation' => 'آیا از حذف فهرست انتخاب شده اطمینان دارید؟',
+ 'no_records' => 'موردی یافت نشد',
+ 'new' => 'فهرست جدید',
+ 'new_name' => 'فهرست جدید',
+ 'new_code' => 'new-menu',
+ 'delete_confirm_single' => 'آیا از حذف این فهرست اطمینان دارید؟',
+ 'saved' => 'فهرست با موفقیت ذخیره شد.',
+ 'name' => 'نام',
+ 'code' => 'کد',
+ 'items' => 'موارد فهرست',
+ 'add_subitem' => 'افزودن زیر فهرست',
+ 'code_required' => 'وارد کردن کد اجباریست',
+ 'invalid_code' => 'قالب کد نا معتبر است. کد میتواند شامل اعداد، حروف لاتین و این کاراکتر ها باشد: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'عنوان',
+ 'editor_title' => 'ویرایش فهرست',
+ 'type' => 'نوع',
+ 'allow_nested_items' => 'استفاده از موارد تو در تو',
+ 'allow_nested_items_comment' => 'موارد تو در تو به صورت خودکار توسط صفحات استاتیک و برخی از دیگر موارد ایجاد می شوند',
+ 'url' => 'آدرس',
+ 'reference' => 'مرجع',
+ 'search_placeholder' => 'جستجوی همه موارد...',
+ 'title_required' => 'وارد کردن عنوان اجباریست',
+ 'unknown_type' => 'نوع نامشخص فهرست',
+ 'unnamed' => 'فهرست بدون نام',
+ 'add_item' => 'افزودن فهرست',
+ 'new_item' => 'مورد جدید برای فهرست',
+ 'replace' => 'جایگرینی این مورد با زیر مورد های ایجاد شده',
+ 'replace_comment' => 'اگر میخواهید زیر فهرست های ایجاد شده هم سطح با این مورد قرار بگیرند این گزینه را فعال نمایید. خود فهرست بصورت خودکار مخفی خواهد شد.',
+ 'cms_page' => 'صفحه ی مدیریت محتوی',
+ 'cms_page_comment' => 'صفحه ای را که میخواهید به هنگام انتخاب این فهرست باز شود را انتخاب نمایید.',
+ 'reference_required' => 'وارد کردن مرجع برای فهرست الزامیست.',
+ 'url_required' => 'وارد کردن آدرس الزامیست',
+ 'cms_page_required' => 'لطفا یک صفحه را انتخاب کنید',
+ 'code' => 'کد',
+ 'code_comment' => 'اگر میخواهید از طریق کد ها به این مورد از فهرست دسترسی پیدا کنید کد آن را وارد نمایید.',
+ 'static_page' => 'صفحات استاتیک',
+ 'all_static_pages' => 'تمام صفحات استاتیک',
+ ],
+ 'content' => [
+ 'menu_label' => 'محتوی',
+ 'cant_save_to_dir' => 'مجوز ذخیره ی داده ها در پوشه ی صفحات استاتسک وجود ندارد.',
+ ],
+ 'sidebar' => [
+ 'add' => 'افزودن',
+ 'search' => 'جستجو...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'نوع شیء نا مشخص است',
+ 'not_found' => 'شیء درخواستی یافت نشد.',
+ ],
+ 'editor' => [
+ 'title' => 'عنوان',
+ 'new_title' => 'عنوان صفحه ی جدید',
+ 'content' => 'محتوی',
+ 'url' => 'آدرس',
+ 'filename' => 'نام فایل',
+ 'layout' => 'طرح بندی',
+ 'description' => 'توضیحات',
+ 'preview' => 'پیش نمایش',
+ 'enter_fullscreen' => 'حالت تمام صفحه',
+ 'exit_fullscreen' => 'خروج از حالت تمام صفحه',
+ 'hidden' => 'مخفی',
+ 'hidden_comment' => 'صفحات مخفی توسط کاربران وارد شده به سایت قابل دسترس می باشند.',
+ 'navigation_hidden' => 'مخفی کردن در فهرست',
+ 'navigation_hidden_comment' => 'اگر میخواهید صفحه مورد نظر در فهرست هایی که خودکار ایجاد می شوند و یا نشان گرهای صفحه دیده نشوند این گزینه را انتخاب نمایید.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'تکه کد',
+ 'code' => 'نام یکتای تکه کد',
+ 'code_comment' => 'نام یکتایی را جهت دسترسی به این بخش در افزونه صفحات استاتیک به عنوان تکه کد وارد نمایید.',
+ 'name' => 'نام',
+ 'name_comment' => 'نام نمایش داده شده این تکه کد در لیست',
+ 'no_records' => 'تکه کدی یافت نشد',
+ 'menu_label' => 'تکه کدها',
+ 'column_property' => 'عنوان مشخصه',
+ 'column_type' => 'نوع',
+ 'column_code' => 'کد یکتا',
+ 'column_default' => 'مقدار پیشفرض',
+ 'column_options' => 'گزینه ها',
+ 'column_type_string' => 'رشته',
+ 'column_type_checkbox' => 'جعبه انتخابی',
+ 'column_type_dropdown' => 'انتخابگر کشویی',
+ 'not_found' => 'تکه کدی با کد یکتای :code در قالب یافت نشد.',
+ 'property_format_error' => 'کد مشخصه باید با یک حرف لاتین شروع شده و شامل حروف لاتین و اعداد می تواند باشد.',
+ 'invalid_option_key' => 'کلید گزینه %s نامعتبر است. کلید گزینه انتخابگر کشویی فقط میتواند شامل اعداد، حروف لاتین وکاراکتر های _ و - باشد.',
+ ],
+ 'component' => [
+ 'static_page_name' => 'صفحات استاتیک',
+ 'static_page_description' => 'نمایش یک صفحه استاتیک در طرح بندی.',
+ 'static_page_use_content_name' => 'استفاده از گرینه ها در محتوی.',
+ 'static_page_use_content_description' => 'اگر غیر فعال باشد، فیلد ها به هنگام ویرایش صفحات استاتیک در بخش محتوی نمایش داده نخواهند شد. محتوی صفحه با استفاده از متغییر های تعریف شده قابل کنتل می باشند.',
+ 'static_page_default_name' => 'طرح بندی پیش فرض',
+ 'static_page_default_description' => 'این طرح بندی به عنوان طرح بندی پیشفرض به هنگام ایجاد صفحه جدید در نظر گرفته شود؟',
+ 'static_page_child_layout_name' => 'طرح بندی صفحات زیرمجموعه',
+ 'static_page_child_layout_description' => 'این طرح بندی به عنوان طرح بندی تمام صفحات زیر مجموعه در نظر گرفنه شود؟',
+ 'static_menu_name' => 'فهرست استاتیک',
+ 'static_menu_description' => 'نمایش فهرست استاتیک در طرح بندی.',
+ 'static_menu_code_name' => 'فهرست',
+ 'static_menu_code_description' => 'کد فهرستی را که میخواهید به نمایش درآید انتخاب نمایید.',
+ 'static_breadcrumbs_name' => 'نشان گرها',
+ 'static_breadcrumbs_description' => 'نمایش نشان گرهای صفحه.',
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/fi/lang.php b/plugins/rainlab/pages/lang/fi/lang.php
new file mode 100644
index 000000000..f9f80eacf
--- /dev/null
+++ b/plugins/rainlab/pages/lang/fi/lang.php
@@ -0,0 +1,133 @@
+ [
+ 'name' => 'Sivut',
+ 'description' => 'Sivu- ja valikko-ominaisuudet.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Sivut',
+ 'template_title' => '%s Sivut',
+ 'delete_confirmation' => 'Haluatko varmasti poistaa valitut sivut? Tämä poistaa myös alasivut, jos sellaisia on.',
+ 'no_records' => 'Sivuja ei löytynyt',
+ 'delete_confirm_single' => 'Haluatko varmasti poistaa tämän sivun? Tämä poistaa myös alasivut, jos sellaisia on.',
+ 'new' => 'Uusi sivu',
+ 'add_subpage' => 'Lisää alasivu',
+ 'invalid_url' => 'Kelvoton URL formaatti. URL pitäisi alkaa kautta -merkillä ja voi sisältää kokonaislukuja, latinalaisia kirjamia, ja seuraavia merkkejä: _-/.',
+ 'url_not_unique' => 'Tämä URL on toisen sivun käyttämä.',
+ 'layout' => 'Ulkoasu',
+ 'layouts_not_found' => 'Ulkoasuja ei löytynyt',
+ 'saved' => 'Sivu on tallennettu onnistuneesti.',
+ 'tab' => 'Sivut',
+ 'manage_pages' => 'Hallitse staattisia sivuja',
+ 'manage_menus' => 'Hallitse staattisia valikkoja',
+ 'access_snippets' => 'Hallitse koodinpätkiä',
+ 'manage_content' => 'Hallitse staattista sisältöä',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Valikot',
+ 'delete_confirmation' => 'Haluatko varmasti poista valitut valikot?',
+ 'no_records' => 'Vakujjiha ei löytynyt',
+ 'new' => 'Uusi valikko',
+ 'new_name' => 'Uusi valikko',
+ 'new_code' => 'uusi-valikko',
+ 'delete_confirm_single' => 'Haluatko varmasti poistaa tämän valikon?',
+ 'saved' => 'Tämä valikko on tallennettu onnistuneesti.',
+ 'name' => 'Nimi',
+ 'code' => 'Koodi',
+ 'items' => 'Valikon kohteet',
+ 'add_subitem' => 'Lisää alakohde',
+ 'code_required' => 'Koodi on vaadittu',
+ 'invalid_code' => 'Koodilla on kelvoton formaatti. Koodi voi sisältää kokonaislukuja, latinalaisia kirjamia, ja seuraavia merkkejä: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Otsikko',
+ 'editor_title' => 'Muokkaa valikkokohdetta',
+ 'type' => 'Tyyppi',
+ 'allow_nested_items' => 'Salli sisäkkäiset kohteet',
+ 'allow_nested_items_comment' => 'Sisäkkäiset kohteet staattisissa sivuissa ja muissa kohdetyypeissä voidaan generoida dynaamisesti',
+ 'url' => 'URL',
+ 'reference' => 'Viite',
+ 'search_placeholder' => 'Hae kaikista viitteistä...',
+ 'title_required' => 'Otsikko on vaadittu',
+ 'unknown_type' => 'Tuntematon valikkokohteen tyyppi',
+ 'unnamed' => 'Nimeämätön valikkokohde',
+ 'add_item' => 'Lisää K ohde',
+ 'new_item' => 'Uusi valikkokohde',
+ 'replace' => 'Korvaa valikko sen generoimilla alikohteilla',
+ 'replace_comment' => 'Käytä tätä valintaruutua työntääksesi valikon kohteet samalle tasolle tämän kohteen kanssa. Tämä kohde itsessään on piilotettu.',
+ 'cms_page' => 'CMS sivu',
+ 'cms_page_comment' => 'Valitse sivu joka avataan, kun valikkokohtaa napsautetaan.',
+ 'reference_required' => 'Valikkokohteen viite on vaadittu.',
+ 'url_required' => 'URL on vaadittu',
+ 'cms_page_required' => 'Valitse CMS sivu',
+ 'code' => 'Koodi',
+ 'code_comment' => 'Syötä valikkokohten koodi jos haluat käyttää sitä API:n kanssa.',
+ 'static_page' => 'Staattinen sivu',
+ 'all_static_pages' => 'Kaikki staattiset sivut'
+ ],
+ 'content' => [
+ 'menu_label' => 'Sisältö',
+ 'cant_save_to_dir' => 'Sisältötiedostojen tallentaminen staatisen-sivujen hakemistoon ei ole sallittua.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Lisää',
+ 'search' => 'Hae...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Tuntematon kohdetyyppi',
+ 'not_found' => 'Pyydettyä kohdetta ei löytynyt.',
+ ],
+ 'editor' => [
+ 'title' => 'Otsikko',
+ 'new_title' => 'Uuden sivun otsikko',
+ 'content' => 'Sisältö',
+ 'url' => 'URL',
+ 'filename' => 'Tiedostonimi',
+ 'layout' => 'Ulkoasu',
+ 'description' => 'Kuvaus',
+ 'preview' => 'Esikatsele',
+ 'enter_fullscreen' => 'Kokoruudun tila',
+ 'exit_fullscreen' => 'Poistu kokoruudun tilasta',
+ 'hidden' => 'Piilotettu',
+ 'hidden_comment' => 'Piilotetut sivut ovat saatavilla ainoastaan hallintaan kirjautuneille.',
+ 'navigation_hidden' => 'Piilota navigaatiossa',
+ 'navigation_hidden_comment' => 'Käytä tätä valintaruutua piilottaaksesi tämä sivu automaattisesti generoiduista valikoista ja leivänmuruista.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Osat',
+ 'code' => 'Osan koodi',
+ 'code_comment' => 'Syötä koodi, jotta tämä osio on käytettävissä Staattisten sivujen -lisäosassa.',
+ 'name' => 'Nimi',
+ 'name_comment' => 'Nimi näkyy osiolistassa Staattisten sivujen -sivupalkissa ja sivuilla kun osa on lisätty.',
+ 'no_records' => 'Osia ei löydy',
+ 'menu_label' => 'Osat',
+ 'column_property' => 'Ominaisuuden otsikko',
+ 'column_type' => 'Tyyppi',
+ 'column_code' => 'Koodi',
+ 'column_default' => 'Oletus',
+ 'column_options' => 'Vaihtoehdot',
+ 'column_type_string' => 'Merkkijono',
+ 'column_type_checkbox' => 'Valintaruutu',
+ 'column_type_dropdown' => 'Alasvetovalikko',
+ 'not_found' => 'Osaa pyydetyllä koodilla :code ei löytynyt teemasta.',
+ 'property_format_error' => 'Ominaisuuden koodin tulisi alkaa latinalaisella kirjaimella ja voi sisältää ainoastaan latinalaisia merkkejä ja kokonaislukuja',
+ 'invalid_option_key' => 'Kelvoton alasvetovalikon vaihtoehtoavain :key. Vaihtoehtojen avaimet voivat sisältää ainoastaan kokonaislukuja, latinalaisia merkkejä, ja merkkejä _ ja -',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Staattinen sivu',
+ 'static_page_description' => 'Näyttää staattisen sivun CMS ulkoasussa.',
+ 'static_page_use_content_name' => 'Käytä sivun sisältökenttää Use page content field',
+ 'static_page_use_content_description' => 'Jos valitsematta, sisältö -kohta ei tule näkyviin staattista sivua muokattaessa. Sivun sisältö määritetään vain paikkamerkkien ja muuttujien kautta.',
+ 'static_page_default_name' => 'Oletusulkoasu',
+ 'static_page_default_description' => 'Määrittelee tämän ulkoasun oletukseksi uusille sivuille',
+ 'static_page_child_layout_name' => 'Alisivun ulkoasu',
+ 'static_page_child_layout_description' => 'Oletusulkoasu kaikille uusille alisivuille',
+ 'static_menu_name' => 'Staattinen valikko',
+ 'static_menu_description' => 'Näyttää valikon CMS ulkoasussa.',
+ 'static_menu_code_name' => 'Valikko',
+ 'static_menu_code_description' => 'Määritä valikon koodi joka pitäisi näyttää.',
+ 'static_breadcrumbs_name' => 'Staattinen leivänmuru',
+ 'static_breadcrumbs_description' => 'Näyttää leivänmuurun staattiselle sivulle.',
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/fr/lang.php b/plugins/rainlab/pages/lang/fr/lang.php
new file mode 100644
index 000000000..ff50f055a
--- /dev/null
+++ b/plugins/rainlab/pages/lang/fr/lang.php
@@ -0,0 +1,143 @@
+ [
+ 'name' => 'Pages',
+ 'description' => 'Fonctionnalités de pages et menus statiques.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Pages',
+ 'template_title' => '%s Pages',
+ 'delete_confirmation' => 'Confirmez-vous la suppression des pages sélectionnées ? Les sous-pages seront également supprimées.',
+ 'no_records' => 'Aucune page trouvée',
+ 'delete_confirm_single' => 'Confirmez-vous la suppression de cette page ? Les sous-pages seront également supprimées.',
+ 'new' => 'Nouvelle page',
+ 'add_subpage' => 'Ajouter une sous-page',
+ 'invalid_url' => 'Le format d’URL est invalide. L’URL doit commencer par un / et peut contenir des chiffres, des lettres et les symboles suivants : _-/.',
+ 'url_not_unique' => 'Cette URL est déjà utilisée par une autre page.',
+ 'layout' => 'Maquette',
+ 'layouts_not_found' => 'Aucune maquette trouvée',
+ 'saved' => 'La page a été sauvegardée avec succès.',
+ 'tab' => 'Pages',
+ 'manage_pages' => 'Gérer les pages statiques',
+ 'manage_menus' => 'Gérer les menus statiques',
+ 'access_snippets' => 'Accès aux fragments',
+ 'manage_content' => 'Gérer le contenu statique'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menus',
+ 'delete_confirmation' => 'Confirmez-vous la suppression des menus sélectionnés ?',
+ 'no_records' => 'Aucun menu trouvé',
+ 'new' => 'Nouveau menu',
+ 'new_name' => 'Nouveau menu',
+ 'new_code' => 'nouveau-menu',
+ 'delete_confirm_single' => 'Confirmez-vous la suppression de ce menu ?',
+ 'saved' => 'Le menu a été sauvegardé avec succès.',
+ 'name' => 'Nom',
+ 'code' => 'Code',
+ 'items' => 'Éléments du menu',
+ 'add_subitem' => 'Ajouter un élément',
+ 'code_required' => 'Le Code est requis',
+ 'invalid_code' => 'Le format du Code est invalide. Le Code peut contenir des chiffres, des lettres et les symboles suivants : _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Titre',
+ 'editor_title' => 'Modifier l’élément du menu',
+ 'type' => 'Type',
+ 'allow_nested_items' => 'Autoriser les sous-éléments',
+ 'allow_nested_items_comment' => 'Les sous-éléments peuvent être générés dynamiquement par les pages statiques et certains des autres types d’élément',
+ 'url' => 'URL',
+ 'reference' => 'Référence',
+ 'search_placeholder' => 'Rechercher toutes les références...',
+ 'title_required' => 'Le Titre est requis',
+ 'unknown_type' => 'Type d’élément du menu inconnu',
+ 'unnamed' => 'Élément de menu sans nom',
+ 'add_item' => 'Ajouter un élément',
+ 'new_item' => 'Nouvel élément du menu',
+ 'replace' => 'Remplacer cet élément part ses sous-éléments générés',
+ 'replace_comment' => 'Utiliser cette case à cocher pour envoyer les sous-éléments générés au même niveau que cet élément. Cet élément sera lui-même masqué.',
+ 'cms_page' => 'Page CMS',
+ 'cms_page_comment' => 'Sélectionnez une page à ouvrir lors d’un clic sur cet élément du menu.',
+ 'reference_required' => 'La référence de l’élément du menu est requis.',
+ 'url_required' => 'L’URL est requise',
+ 'cms_page_required' => 'Sélectionnez une page CMS s’il vous plaît',
+ 'display_tab' => 'Affichage',
+ 'hidden' => 'Caché',
+ 'hidden_comment' => "Empêcher ce menu d'apparaître sur le site web.",
+ 'attributes_tab' => 'Attributs',
+ 'code' => 'Code',
+ 'code_comment' => 'Entrez le code de l’élément du menu si vous souhaitez y accéder via l’API.',
+ 'css_class' => 'Classe CSS',
+ 'css_class_comment' => 'Entrez un nom de classe CSS pour donner à cet élément de menu une apparence personnalisée.',
+ 'external_link' => 'Lien externe',
+ 'external_link_comment' => 'Ouvrir les liens pour ce menu dans une nouvelle fenêtre.',
+ 'static_page' => 'Page Statique',
+ 'all_static_pages' => 'Toutes les pages'
+ ],
+ 'content' => [
+ 'menu_label' => 'Contenu',
+ 'cant_save_to_dir' => 'L’enregistrement des fichiers de contenu dans le répertoire des pages statiques n’est pas autorisé.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Ajouter',
+ 'search' => 'Rechercher...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Type d’objet inconnu',
+ 'not_found' => 'L’objet demandé n’a pas été trouvé.'
+ ],
+ 'editor' => [
+ 'title' => 'Titre',
+ 'new_title' => 'Nouveau titre de la page',
+ 'content' => 'Contenu',
+ 'url' => 'URL',
+ 'filename' => 'Nom du fichier',
+ 'layout' => 'Maquette',
+ 'description' => 'Description',
+ 'preview' => 'Aperçu',
+ 'enter_fullscreen' => 'Activer le mode plein écran',
+ 'exit_fullscreen' => 'Annuler le mode plein écran',
+ 'hidden' => 'Caché',
+ 'hidden_comment' => 'Les pages cachées sont seulement accessibles aux administrateurs connectés.',
+ 'navigation_hidden' => 'Masquer dans la navigation',
+ 'navigation_hidden_comment' => 'Cochez cette case pour masquer cette page dans les menus et le fil d’ariane générés automatiquement.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Fragment',
+ 'code' => 'Code du fragment',
+ 'code_comment' => 'Entrez un code pour rendre ce contenu partiel disponible en tant que fragment dans le plugin des Pages Statiques.',
+ 'name' => 'Nom',
+ 'name_comment' => 'Le nom est affiché dans la liste des fragments dans le menu latéral des Pages Statiques et dans une Page lorsque qu’un fragment y est ajouté.',
+ 'no_records' => 'Aucun fragment trouvé',
+ 'menu_label' => 'Fragments',
+ 'column_property' => 'Nom de la propriété',
+ 'column_type' => 'Type',
+ 'column_code' => 'Code',
+ 'column_default' => 'Valeur par défaut',
+ 'column_options' => 'Options',
+ 'column_type_string' => 'Chaîne de caractères',
+ 'column_type_checkbox' => 'Case à cocher',
+ 'column_type_dropdown' => 'Menu déroulant',
+ 'not_found' => 'Le fragment demandé avec le code :code n’a pas été trouvé dans le thème.',
+ 'property_format_error' => 'Le code de la propriété devrait commencer par une lettre et ne peut contenir que des lettres et des chiffres',
+ 'invalid_option_key' => 'Clé de l’option de la liste déroulante invalide. Les clés des options ne peuvent contenir que des chiffres, des lettres et les symboles _ et -'
+ ],
+ 'component' => [
+ 'static_page_name' => 'Page Statique',
+ 'static_page_description' => 'Affiche une page statique dans une maquette du CMS.',
+ 'static_page_use_content_name' => 'Affiche la section de contenu',
+ 'static_page_use_content_description' => 'Si cette case n\'est pas cochée, la section de contenu n\'apparaîtra pas lors de la modification de la page statique. Le contenu de la page sera déterminé uniquement à l\'aide d\'espaces réservés et de variables.',
+ 'static_page_default_name' => 'Disposition par défault',
+ 'static_page_default_description' => 'Définit cette mise en page par défault pour les nouvelles pages',
+ 'static_page_child_layout_name' => 'Mise en page de la sous-page',
+ 'static_page_child_layout_description' => 'La mise en page à utiliser par défault pour les nouvelles sous-pages',
+ 'static_menu_name' => 'Menu Statique',
+ 'static_menu_description' => 'Affiche un menu dans une maquette du CMS.',
+ 'static_menu_code_name' => 'Menu',
+ 'static_menu_code_description' => 'Spécifiez le code du menu que le composant devrait afficher.',
+ 'static_breadcrumbs_name' => 'Breadcrumbs statique',
+ 'static_breadcrumbs_description' => 'Affiche l\' aide à la navigation dans une page statique.',
+ 'child_pages_name' => 'Pages enfants',
+ 'child_pages_description' => 'Affiche une liste de pages enfants pour la page en cours',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/hu/lang.php b/plugins/rainlab/pages/lang/hu/lang.php
new file mode 100644
index 000000000..6c4238c1d
--- /dev/null
+++ b/plugins/rainlab/pages/lang/hu/lang.php
@@ -0,0 +1,145 @@
+ [
+ 'name' => 'Oldalak',
+ 'description' => 'Oldalak, menük, tartalmak és kódrészletek menedzselése.'
+ ],
+ 'page' => [
+ 'menu_label' => 'Oldalak',
+ 'template_title' => '%s Oldalak',
+ 'delete_confirmation' => 'Valóban törölni akarja a kijelölt oldalakat és azok aloldalait?',
+ 'no_records' => 'Nincs létrehozva oldal',
+ 'delete_confirm_single' => 'Valóban törölni akarja ezt az oldalt és aloldalait?',
+ 'new' => 'Új oldal',
+ 'add_subpage' => 'Aloldal hozzáadása',
+ 'invalid_url' => 'Érvénytelen a webcím formátuma. Perjellel kell kezdődnie, és számokat, latin betűket, valamint a következő szimbólumokat tartalmazhatja: _-/.',
+ 'url_not_unique' => 'Egy másik oldal már használja ezt a webcímet.',
+ 'layout' => 'Elrendezés',
+ 'layouts_not_found' => 'Nincs létrehozva elrendezés.',
+ 'saved' => 'Az oldal mentése sikerült.',
+ 'tab' => 'Oldalak',
+ 'manage_pages' => 'Oldalak kezelése',
+ 'manage_menus' => 'Menük kezelése',
+ 'access_snippets' => 'Kódrészletek kezelése',
+ 'manage_content' => 'Tartalom kezelése'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menük',
+ 'delete_confirmation' => 'Valóban törölni akarja a kijelölt menüket?',
+ 'no_records' => 'Nincs létrehozva menü',
+ 'new' => 'Új menü',
+ 'new_name' => 'Új menü',
+ 'new_code' => 'uj-menu',
+ 'delete_confirm_single' => 'Valóban törölni akarja ezt a menüt?',
+ 'saved' => 'A menü mentése sikerült.',
+ 'name' => 'Név',
+ 'code' => 'Kód',
+ 'items' => 'Menüpont',
+ 'add_subitem' => 'Almenü hozzáadása',
+ 'code_required' => 'A Kód kötelező',
+ 'invalid_code' => 'Érvénytelen a kód formátuma. Csak számokat, latin betűket és a következő szimbólumokat tartalmazhatja: _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Cím',
+ 'editor_title' => 'Menüpont szerkesztése',
+ 'type' => 'Típus',
+ 'allow_nested_items' => 'Beágyazott menüpontok engedélyezése',
+ 'allow_nested_items_comment' => 'A beágyazott menüpontokat az oldal és néhány más menüpont típus dinamikusan generálhatja',
+ 'url' => 'Webcím',
+ 'reference' => 'Hivatkozás',
+ 'search_placeholder' => 'Keresés...',
+ 'title_required' => 'A cím megadása kötelező',
+ 'unknown_type' => 'Ismeretlen menüponttípus',
+ 'unnamed' => 'Névtelen menüpont',
+ 'add_item' => 'M enüpont hozzáadása',
+ 'new_item' => 'Új menüpont',
+ 'replace' => 'A menüpont kicserélése a generált gyermekeire',
+ 'replace_comment' => 'Ennek a jelölőnégyzetnek a használatával viheti a generált menüpontokat az ezen menüpont által azonos szintre. Maga ez a menüpont rejtett marad.',
+ 'cms_page' => 'Oldal',
+ 'cms_page_comment' => 'Válassza ki a menüre kattintáskor megnyitni kívánt oldalt.',
+ 'reference_required' => 'A menüpont hivatkozás kitöltése kötelező.',
+ 'url_required' => 'A webcím megadása kötelező',
+ 'cms_page_required' => 'Válasszon egy oldalt',
+ 'display_tab' => 'Megjelenés',
+ 'hidden' => 'Rejtett',
+ 'hidden_comment' => 'Nem jelenik meg a felhasználói felületen.',
+ 'attributes_tab' => 'Tulajdonságok',
+ 'code' => 'Kód',
+ 'code_comment' => 'Az API eléréshez szükséges egyedi azonosító.',
+ 'css_class' => 'CSS osztály',
+ 'css_class_comment' => 'Egyedi megjelenés esetén szükséges megadni.',
+ 'external_link' => 'Külső hivatkozás',
+ 'external_link_comment' => 'A link új ablakban fog megjelenni.',
+ 'static_page' => 'Oldalak',
+ 'all_static_pages' => 'Összes oldal'
+ ],
+ 'content' => [
+ 'menu_label' => 'Tartalom',
+ 'saved' => 'A tartalom mentése sikerült.',
+ 'cant_save_to_dir' => 'A fájlok mentése a "static-pages" könyvtárba nem engedélyezett.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Hozzáadás',
+ 'search' => 'Keresés...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Ismeretlen objektumtípus',
+ 'unauthorized_type' => 'Nem jogosult a következő objektum(ok) kezelésére: :type',
+ 'not_found' => 'A kért objektum nem található.'
+ ],
+ 'editor' => [
+ 'title' => 'Cím',
+ 'new_title' => 'Új oldal címe',
+ 'content' => 'Tartalom',
+ 'url' => 'Webcím',
+ 'filename' => 'Fájlnév',
+ 'layout' => 'Elrendezés',
+ 'description' => 'Leírás',
+ 'preview' => 'Előnézet',
+ 'enter_fullscreen' => 'Váltás teljes képernyős módra',
+ 'exit_fullscreen' => 'Kilépés a teljes képernyős módból',
+ 'hidden' => 'Rejtett',
+ 'hidden_comment' => 'A rejtett oldalakhoz csak a bejelentkezett kiszolgáló oldali felhasználók férhetnek hozzá.',
+ 'navigation_hidden' => 'Elrejtés a navigációban',
+ 'navigation_hidden_comment' => 'Jelölje be ezt a jelölőnégyzetet ennek a oldalnak az automatikusan generált menükből és útkövetésekből való elrejtéséhez.'
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Kódrészlet',
+ 'code' => 'Kódrészlet kódja',
+ 'code_comment' => 'Adja meg a kódot, hogy a jelenlegi részlap elérhető legyen kódrészletként a Oldalak bővítményben.',
+ 'name' => 'Név',
+ 'name_comment' => 'A Kódrészletek listában jelenik meg a Oldalak oldalsó menüjében, valamint a Oldalak aloldalon.',
+ 'no_records' => 'Nincs létrehozva kódrészlet',
+ 'menu_label' => 'Kódrészletek',
+ 'column_property' => 'Cím',
+ 'column_type' => 'Típus',
+ 'column_code' => 'kód',
+ 'column_default' => 'Alapértelmezett',
+ 'column_options' => 'Lehetőségek',
+ 'column_type_string' => 'Szöveg',
+ 'column_type_checkbox' => 'Jelölőnégyzet',
+ 'column_type_dropdown' => 'Lenyíló lista',
+ 'not_found' => 'A(z) :code nevű kódrészlet nem található a témában.',
+ 'property_format_error' => 'A kód latin karakterrel kezdődhet és csak latin karaktereket és számokat tartalmazhat.',
+ 'invalid_option_key' => 'Érvénytelen formátum: :key. Csak számokat, latin betűket és a következő szimbólumokat tartalmazhatja: _-'
+ ],
+ 'component' => [
+ 'static_page_name' => 'Statikus oldal',
+ 'static_page_description' => 'Oldalak megjelenítése.',
+ 'static_page_use_content_name' => 'Tartalom mező használata',
+ 'static_page_use_content_description' => 'Ha nem engedélyezi ezt, akkor a tartalmi rész nem fog megjelenni az oldal szerkesztésénél. Az oldal tartalmát kizárólag a változók fogják meghatározni.',
+ 'static_page_default_name' => 'Alapértelmezett elrendezés',
+ 'static_page_default_description' => 'Minden új oldal ezt az elrendezést fogja hasznáni alapértelmezettként.',
+ 'static_page_child_layout_name' => 'Aloldal elrendezés',
+ 'static_page_child_layout_description' => 'Minden új aloldal ezt az elrendezést fogja használni alapértelmezettként.',
+ 'static_menu_name' => 'Statikus menü',
+ 'static_menu_description' => 'Menük megjelenítése.',
+ 'static_menu_code_name' => 'Menü',
+ 'static_menu_code_description' => 'Speciális kód a megjelenő menünek.',
+ 'static_breadcrumbs_name' => 'Statikus kenyérmorzsa',
+ 'static_breadcrumbs_description' => 'Kenyérmorzsa megjelenítése.',
+ 'child_pages_name' => 'Aloldalak',
+ 'child_pages_description' => 'Megjeleníti az aktuális oldal aloldalainak listáját.',
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/it/lang.php b/plugins/rainlab/pages/lang/it/lang.php
new file mode 100644
index 000000000..f98787730
--- /dev/null
+++ b/plugins/rainlab/pages/lang/it/lang.php
@@ -0,0 +1,116 @@
+ [
+ 'name' => 'Pages',
+ 'description' => 'Funzionalità di pagine & menu.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Pagine',
+ 'template_title' => '%s Pagine',
+ 'delete_confirmation' => 'Vuoi davvero eliminare le pagine selezionate? L\'operazione cancellerà anche le sottopagine, se presenti.',
+ 'no_records' => 'Nessuna pagina trovata',
+ 'delete_confirm_single' => 'Vuoi davvero eliminare questa pagina? L\'operazione cancellerà anche le sottopagine, se presenti.',
+ 'new' => 'Nuova pagina',
+ 'add_subpage' => 'Aggiungi sottopagina',
+ 'invalid_url' => 'Formato dell\'URL non valido. L\'URL deve iniziare con una barra e può contenere numeri, lettere latine e i seguenti simboli: _-/.',
+ 'url_not_unique' => 'L\'URL è già utilizzato da un\'altra pagina.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Layouts non trovato',
+ 'saved' => 'Pagina salvata con successo.',
+ 'tab' => 'Pagine',
+ 'manage_pages' => 'Gestisci pagine',
+ 'manage_menus' => 'Gestisci menu',
+ 'access_snippets' => 'Accedi agli snippet',
+ 'manage_content' => 'Gestisci contenuti'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menu',
+ 'delete_confirmation' => 'Vuoi davvero eliminare i menu selezionati?',
+ 'no_records' => 'Nessun menu trovato',
+ 'new' => 'Nuovo menu',
+ 'new_name' => 'Nuovo menu',
+ 'new_code' => 'nuovo-menu',
+ 'delete_confirm_single' => 'Vuoi davvero eliminare questo menu?',
+ 'saved' => 'Menu salvato con successo.',
+ 'name' => 'Nome',
+ 'code' => 'Codice',
+ 'items' => 'Voci di menu',
+ 'add_subitem' => 'Aggiungi sottomenu',
+ 'code_required' => 'Il Codice è obbligatorio',
+ 'invalid_code' => 'Formato del Codice non valido. Il Codice può contenere numeri, lettere latine e i seguenti simboli: _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Titolo',
+ 'editor_title' => 'Modifica voce di menu',
+ 'type' => 'Tipo',
+ 'allow_nested_items' => 'Consenti elementi nidificati',
+ 'allow_nested_items_comment' => 'Gli elementi nidificati possono essere generati dinamicamente dalle pagine e altre tipologie di elementi',
+ 'url' => 'URL',
+ 'reference' => 'Riferimento',
+ 'title_required' => 'Il Titolo è obbligatorio',
+ 'unknown_type' => 'Tipologia di menu sconosciuta',
+ 'unnamed' => 'Voce di menu senza nome',
+ 'add_item' => 'Aggiungi elemento',
+ 'new_item' => 'Nuova voce di menu',
+ 'replace' => 'Sostituisci questo elemento con i figli generati',
+ 'replace_comment' => 'Usa questa checkbox per inserire le voci di menu generate allo stesso livello di questo elemento. Questa voce verrà nascosta.',
+ 'cms_page' => 'Pagine CMS',
+ 'cms_page_comment' => 'Seleziona una pagina del CMS da aprire quando viene selezionata la voce di menu.',
+ 'reference_required' => 'Il riferimento della voce di menu è obbligatorio.',
+ 'url_required' => 'L\'URL è obbligatorio',
+ 'cms_page_required' => 'Seleziona una pagina CMS',
+ 'code' => 'Codice',
+ 'code_comment' => 'Inserisci il codice della voce di menu se vuoi accedervi con l\'API.',
+ 'static_page' => 'Pagine',
+ 'all_static_pages' => 'Tutte le pagine'
+ ],
+ 'content' => [
+ 'menu_label' => 'Contenuti',
+ 'cant_save_to_dir' => 'Salvataggio dei file di contenuto nella directory static-pages non consentito.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Aggiungi',
+ 'search' => 'Cerca...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Tipo di oggetto sconosciuto',
+ 'not_found' => 'Oggetto richiesto non trovato.'
+ ],
+ 'editor' => [
+ 'title' => 'Titolo',
+ 'new_title' => 'Titolo nuova pagina',
+ 'content' => 'Contenuto',
+ 'url' => 'URL',
+ 'filename' => 'Nome file',
+ 'layout' => 'Layout',
+ 'description' => 'Descrizione',
+ 'preview' => 'Anteprima',
+ 'enter_fullscreen' => 'Abilita visualizzazione a schermo intero',
+ 'exit_fullscreen' => 'Esci dalla visualizzazione a schermo intero',
+ 'hidden' => 'Nascosto',
+ 'hidden_comment' => 'Le pagine nascoste sono accessibili soltanto dagli utenti che hanno effettuato l\'accesso al pannello di controllo.',
+ 'navigation_hidden' => 'Nascondi dalla navigazione',
+ 'navigation_hidden_comment' => 'Seleziona questa checkbox per nascondere questa pagina dai menu e dalle barre di navigazione generate automaticamente.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Codice Snippet',
+ 'code_comment' => 'Inserisci un codice per rendere questa vista parziale disponibile come snippet nel plugin Static Pages.',
+ 'name' => 'Nome',
+ 'name_comment' => 'Il nome è visualizzato nella lista degli snippet nella barra laterale di Static Pages e su una Pagina quando viene aggiunto lo snippet.',
+ 'no_records' => 'Nessuno snippet trovato',
+ 'menu_label' => 'Snippet',
+ 'column_property' => 'Titolo proprietà',
+ 'column_type' => 'Tipo',
+ 'column_code' => 'Codice',
+ 'column_default' => 'Default',
+ 'column_options' => 'Opzioni',
+ 'column_type_string' => 'Testo',
+ 'column_type_checkbox' => 'Checkbox',
+ 'column_type_dropdown' => 'Menu a cascata',
+ 'not_found' => 'Snippet con codice :code non trovato nel tema.',
+ 'property_format_error' => 'Il codice della proprietà deve iniziare con una lettera latina e può contenere solo lettere latine e numeri',
+ 'invalid_option_key' => 'Opzione del menu a cascata non valida: %s. Le opzioni possono contenere solo numeri, lettere latine e i caratteri _ e -'
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/lv/lang.php b/plugins/rainlab/pages/lang/lv/lang.php
new file mode 100644
index 000000000..350554bc5
--- /dev/null
+++ b/plugins/rainlab/pages/lang/lv/lang.php
@@ -0,0 +1,120 @@
+ [
+ 'name' => 'Lapas',
+ 'description' => 'Lapu un izvēļņu funkcijas.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Lapas',
+ 'template_title' => '%s Lapas',
+ 'delete_confirmation' => 'Vai tu tiešām vēlies dzēst izvēlētās lapas? Šī operācija izdzēsīs arī apakšlapas (ja tādas ir).',
+ 'no_records' => 'Neviena lapa netika atrasta',
+ 'delete_confirm_single' => 'Vai tu tiešām vēlies dzēst izvēlēto lapu? Šī operācija izdzēsīs arī apakšlapas (ja tādas ir).',
+ 'new' => 'Jauna lapa',
+ 'add_subpage' => 'Pievienot apakšlapu',
+ 'invalid_url' => 'Nekorekts saites formāts. Saitei vajadzētu sākties ar slīpsvītru un tā var saturēt ciparus, latīnu alfabēta burtus, slīpsvītras un sekojošos sibolus: _-/.',
+ 'url_not_unique' => 'Šādu saiti izmanto jau kāda cita lapa.',
+ 'layout' => 'Izkārtojums',
+ 'layouts_not_found' => 'Izkārtojumi netika atrasti',
+ 'saved' => 'Lapa tika veiksmīgi saglabāta.',
+ 'tab' => 'Lapas',
+ 'manage_pages' => 'Pieeja labot statiskās lapas',
+ 'manage_menus' => 'Pieeja labot statiskās izvēlnes',
+ 'access_snippets' => 'Pieeja koda fragmentiem',
+ 'manage_content' => 'Pieeja labot statisko saturu',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Izvēlnes',
+ 'delete_confirmation' => 'Vai tu tiešām vēlies dzēst izvēlētās izvēlnes?',
+ 'no_records' => 'Izvēlnes netika atrastas',
+ 'new' => 'Jauna izvēlne',
+ 'new_name' => 'Jauna izvēlne',
+ 'new_code' => 'jauna-izvelne',
+ 'delete_confirm_single' => 'Vai tu tiešām vēlies dzēst šo izvēlni?',
+ 'saved' => 'Izvēlne tika veiksmīgi saglabāta',
+ 'name' => 'Vārds',
+ 'code' => 'Kods',
+ 'items' => 'Izvēlnes priekšmeti',
+ 'add_subitem' => 'Pievienot apakšpriekšmetu',
+ 'code_required' => 'Kods ir obligāts lauks.',
+ 'invalid_code' => 'Nepreaizs koda formāts. Kods var saturēt ciparus, latīnu alfabēta burtus un sekojošos simbolus: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Nosaukums',
+ 'editor_title' => 'Labot izvēlnes priekšmetu',
+ 'type' => 'Tips',
+ 'allow_nested_items' => 'Atļaut iegultos priekšmetus',
+ 'allow_nested_items_comment' => 'Iegultie priekšmeti var tikt dinamiski ģenerēti',
+ 'url' => 'Saite',
+ 'reference' => 'Atsauce',
+ 'title_required' => 'Nosaukuma lauks ir obligāts',
+ 'unknown_type' => 'Nezināms izvēlnes tips',
+ 'unnamed' => 'Izvēlnes priekšmets bez nosaukums',
+ 'add_item' => 'Pievienot Pri ekšmetu',
+ 'new_item' => 'Jauns izvēlnes priekšmets',
+ 'replace' => 'Aizvietot šo priekšmetu ar tā ģenerētajiem bērniem',
+ 'cms_page' => 'CMS lapa',
+ 'cms_page_comment' => 'Izvēlies lapu, kas atvērsies, kad tiks noklikšķiāts uz šī izvēlnes priekšmeta.',
+ 'reference_required' => 'Izvēlnes priekšmeta atsauce ir obligāts lauks.',
+ 'url_required' => 'Saite ir obligāti jāievada',
+ 'cms_page_required' => 'Lūdzu, izvēlies CMS lapu',
+ 'code' => 'Kods',
+ 'code_comment' => 'Ievadi izvēlnes priekšmeta kodu, ja tam vēlies piekļūt izmantojot API.',
+ 'static_page' => 'Statiska lapa',
+ 'all_static_pages' => 'Visas statiskās lapas'
+ ],
+ 'content' => [
+ 'menu_label' => 'Saturs',
+ 'cant_save_to_dir' => 'Satura failu saglabāšana statisko lapu direktorijā nav atļauta.S',
+ ],
+ 'sidebar' => [
+ 'add' => 'Pievienot',
+ 'search' => 'Meklēt...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Nezināms objekta tips',
+ 'not_found' => 'Pieprasītais objekts netika atrasts.',
+ ],
+ 'editor' => [
+ 'title' => 'Nosaukums',
+ 'new_title' => 'Jaunās lapas nosaukums',
+ 'content' => 'Saturs',
+ 'url' => 'Saite',
+ 'filename' => 'Faila nosaukums',
+ 'layout' => 'Izkārtojums',
+ 'description' => 'Skaidrojums',
+ 'preview' => 'Priekšskats',
+ 'enter_fullscreen' => 'Atvērt pilnekrāna režīmu',
+ 'exit_fullscreen' => 'Aizvērt pilnekrāna režīmu',
+ 'hidden' => 'Paslēpts',
+ 'hidden_comment' => 'Paslēptās lapas varēs redzēt tikai ielogojušies back-end lietotāji.',
+ 'navigation_hidden' => 'Paslēpt navigācijā',
+ 'navigation_hidden_comment' => 'Atķeksē šo kasti, lai automātiski ģenerētu izvēlnes un breadcrumbus.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Koda fragmenti',
+ 'code' => 'Koda fragmenta kods',
+ 'code_comment' => 'Ievadi kodu, lai padarītu šo partialu par koda fragmentu.',
+ 'name' => 'Nosaukums',
+ 'name_comment' => 'Nosaukums tiks rādīts koda fragmentu sarakstā.',
+ 'no_records' => 'Koda fragmenti netika atrasti',
+ 'menu_label' => 'Koda fragmenti',
+ 'column_property' => 'Nosaukums',
+ 'column_type' => 'Tips',
+ 'column_code' => 'Kods',
+ 'column_default' => 'Noklusējuma vērtība',
+ 'column_options' => 'Opcijas',
+ 'column_type_string' => 'Teksts',
+ 'column_type_checkbox' => 'Checkboksis',
+ 'column_type_dropdown' => 'Dropdowns',
+ 'not_found' => 'Koda fragments ar pieprasīto kodu :code netika atrasts tēmā.',
+ 'property_format_error' => 'Kodam vajadzētu sākties ar latīņu alfabēta burtu un tas var saturēt latīņu alfabēta burtus un ciparus',
+ 'invalid_option_key' => 'Nekorekta dropdowna vērtība: :key.',
+ ],
+ 'component' => [
+ 'static_page_description' => 'Izvada statisku lapu CMS iegultnē.',
+ 'static_menu_description' => 'Izvada izvēlni CMS iegultnē.',
+ 'static_menu_menu_code' => 'Specificē komponentes kodu, ko izvadīt',
+ 'static_breadcrumbs_description' => 'Izvada breadcrumbus CMS iegultnē.',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/nb-no/lang.php b/plugins/rainlab/pages/lang/nb-no/lang.php
new file mode 100644
index 000000000..797db80d3
--- /dev/null
+++ b/plugins/rainlab/pages/lang/nb-no/lang.php
@@ -0,0 +1,114 @@
+ [
+ 'name' => 'Sider',
+ 'description' => 'Side- og menyfunksjoner.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Sider',
+ 'template_title' => '%s Sider',
+ 'delete_confirmation' => 'Vil du virkelig slette de valgte sidene? Hvis siden har undersider, blir de også slettet.',
+ 'no_records' => 'Ingen sider funnet',
+ 'delete_confirm_single' => 'Vil du virkelig slette denne siden? Hvis siden har undersider, blir de også slettet.',
+ 'new' => 'Ny side',
+ 'add_subpage' => 'Ny underside',
+ 'invalid_url' => 'Ugyldig URL-format. URL-en skal starte med en skråstrek og kan bare inneholde tall, latinske bokstaver og følgende symboler: _-/.',
+ 'url_not_unique' => 'URL-en er allerede i bruk av en annen side.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Ingen layouts funnet',
+ 'saved' => 'Siden har blitt lagret.',
+ 'manage_pages' => 'Administrer statiske sider',
+ 'manage_menus' => 'Administrer statiske menyer',
+ 'access_snippets' => 'Tilgang til snippets',
+ 'manage_content' => 'Administrer statisk innhold'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menyer',
+ 'delete_confirmation' => 'Vil du virkelig slette valgte menyer?',
+ 'no_records' => 'Ingen menyer funnet',
+ 'new' => 'Ny meny',
+ 'new_name' => 'Ny meny',
+ 'new_code' => 'new-menu',
+ 'delete_confirm_single' => 'Vil du virkelig slette denne menyen?',
+ 'saved' => 'Menyen har blitt lagret.',
+ 'name' => 'Navn',
+ 'code' => 'Kode',
+ 'items' => 'Elementer',
+ 'add_subitem' => 'Nytt underelement',
+ 'no_records' => 'Ingen elementer funnet',
+ 'code_required' => 'En kode kreves.',
+ 'invalid_code' => 'Ugyldig kode-format. Koden kan inneholde tall, latinske bokstaver og følgende symboler: _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Tittel',
+ 'editor_title' => 'Endre element',
+ 'type' => 'Type',
+ 'allow_nested_items' => 'Tillat underelementer',
+ 'allow_nested_items_comment' => 'Underelementer kan bli generert dynamisk av statiske sider og andre elementtyper',
+ 'url' => 'URL',
+ 'reference' => 'Referanse',
+ 'title_required' => 'Tittel kreves.',
+ 'unknown_type' => 'Ukjent elementtype',
+ 'unnamed' => 'Navnløs elementtype',
+ 'add_item' => 'Legg til e lement',
+ 'new_item' => 'Nytt element',
+ 'replace' => 'Erstatt dette elementet med sine underelementer',
+ 'replace_comment' => 'Huk av denne boksen for å skjule dette elementet. Underelementer blir fremdeles synlige.',
+ 'cms_page' => 'CMS-side',
+ 'cms_page_comment' => 'Velg hvilken side som skal åpnes når man trykker på linken.',
+ 'reference_required' => 'En referanse kreves.',
+ 'url_required' => 'En URL kreves.',
+ 'cms_page_required' => 'Vennligst velg en CMS-side',
+ 'code' => 'Kode',
+ 'code_comment' => 'Velg en elementkode hvis du trenger tilgang via API-en. (valgfritt)'
+ ],
+ 'content' => [
+ 'menu_label' => 'Innhold',
+ 'cant_save_to_dir' => 'Å lagre innhold til files i static-pages-mappen er ikke tillatt.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Legg til',
+ 'search' => 'Søk...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Ukjent objekttype',
+ 'not_found' => 'Det forespurte objektet ble ikke funnet.'
+ ],
+ 'editor' => [
+ 'title' => 'Tittel',
+ 'new_title' => 'Tittel på siden',
+ 'content' => 'Innhold',
+ 'url' => 'URL',
+ 'filename' => 'Filnavn',
+ 'layout' => 'Layout',
+ 'description' => 'Beskrivelse',
+ 'preview' => 'Forhåndsvis',
+ 'enter_fullscreen' => 'Fullskjermmodus',
+ 'exit_fullscreen' => 'Avslutt fullskjermmodus',
+ 'hidden' => 'Skjult',
+ 'hidden_comment' => 'Kun backend-brukere har tilgang til skjulte sider.',
+ 'navigation_hidden' => 'Gjem i menyer',
+ 'navigation_hidden_comment' => 'Huk av denne boksen for å skjule denne siden i genererte menyer',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Snippet-kode',
+ 'code_comment' => 'Fyll inn en kode for å gjøre denne tilgjengelig som en snippet i Static Pages.',
+ 'name' => 'Navn',
+ 'name_comment' => 'Navnet som blir presentert i snippetlisten i Static Pages-menyen og når snippeten er lagt inn på en side.',
+ 'no_records' => 'Ingen snippets funnet',
+ 'menu_label' => 'Snippets',
+ 'column_property' => 'Egenskap',
+ 'column_type' => 'Type',
+ 'column_code' => 'Kode',
+ 'column_default' => 'Standard',
+ 'column_options' => 'Alternativer',
+ 'column_type_string' => 'String',
+ 'column_type_checkbox' => 'Checkbox',
+ 'column_type_dropdown' => 'Dropdown',
+ 'not_found' => 'En snippet med koden :code ble ikke funnet.',
+ 'property_format_error' => 'Egenskapkoden skal starte med en latinsk bokstav og kan kun inneholde bokstver og tall.',
+ 'invalid_option_key' => 'Ugyldig dropdown-alternativ kode: %s. Alternativkoder kan bare inneholde tall, latinske bokstaver, _ og -'
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/nl/lang.php b/plugins/rainlab/pages/lang/nl/lang.php
new file mode 100644
index 000000000..13ea5c0b1
--- /dev/null
+++ b/plugins/rainlab/pages/lang/nl/lang.php
@@ -0,0 +1,116 @@
+ [
+ 'name' => 'Pagina\'s',
+ 'description' => 'Pagina & menu functionaliteit.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Pagina\'s',
+ 'template_title' => '%s Pagina\'s',
+ 'delete_confirmation' => 'Weet u zeker dat u de geselecteerde pagina\'s wilt verwijderen? Ook eventuele subpagina\'s zullen hierdoor verwijderd worden.',
+ 'no_records' => 'Geen pagina\'s gevonden',
+ 'delete_confirm_single' => 'Weet u zeker dat u deze pagina wilt verwijderen? Ook eventuele subpagina\'s zullen hierdoor verwijderd worden.',
+ 'new' => 'Nieuwe pagina',
+ 'add_subpage' => 'Subpagina toevoegen',
+ 'invalid_url' => 'Ongeldige URL-structuur. De URL moet beginnen met een slash en kan enkel cijfers, Latijnse letters en deze symbolen bevatten: _-/.',
+ 'url_not_unique' => 'Deze URL wordt al gebruikt door een andere pagina.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Geen layouts gevonden',
+ 'saved' => 'De pagina is succesvol opgeslagen.',
+ 'tab' => 'Pagina\'s',
+ 'manage_pages' => 'Beheer statische pagina\'s',
+ 'manage_menus' => 'Beheer statische menu\'s',
+ 'access_snippets' => 'Toegang tot blokken',
+ 'manage_content' => 'Beheer statische inhoud',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menu\'s',
+ 'delete_confirmation' => 'Weet u zeker dat u de geselecteerde menu\'s wilt verwijderen?',
+ 'no_records' => 'Geen menu\'s gevonden',
+ 'new' => 'Nieuw menu',
+ 'new_name' => 'Nieuw menu',
+ 'new_code' => 'nieuw-menu',
+ 'delete_confirm_single' => 'Weet u zeker dat u dit menu wilt verwijderen?',
+ 'saved' => 'Het menu is opgeslagen.',
+ 'name' => 'Naam',
+ 'code' => 'Code',
+ 'items' => 'Menu items',
+ 'add_subitem' => 'Subitem toevoegen',
+ 'code_required' => 'Code is verplicht',
+ 'invalid_code' => 'Ongeldige code-structuur. De Code kan enkel cijfers, Latijnse letters en deze symbolen bevatten: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Titel',
+ 'editor_title' => 'Bewerk Menu Item',
+ 'type' => 'Type',
+ 'allow_nested_items' => 'Accepteer geneste items',
+ 'allow_nested_items_comment' => 'Geneste items worden dynamisch gegenereerd door statische pagina\'s en sommige andere types.',
+ 'url' => 'URL',
+ 'reference' => 'Referentie',
+ 'title_required' => 'Titel is verplicht',
+ 'unknown_type' => 'Onbekend menu item type',
+ 'unnamed' => 'Onbenoemd menu item',
+ 'add_item' => 'I tem toevoegen',
+ 'new_item' => 'Nieuw menu item',
+ 'replace' => 'Vervang dit item door de gegenereerde subitems',
+ 'replace_comment' => 'Wanneer u deze optie aanvinkt, zullen de gegenereerd menu items worden getoond op het niveau van dit item. Dit item zelf zal verborgen blijven.',
+ 'cms_page' => 'CMS Pagina',
+ 'cms_page_comment' => 'Selecteer een pagina om te openen wanneer op het menu item geklikt wordt.',
+ 'reference_required' => 'Een referentie is verplicht.',
+ 'url_required' => 'Een URL is verplicht',
+ 'cms_page_required' => 'Gelieve een CMS Pagina te selecteren',
+ 'code' => 'Code',
+ 'code_comment' => 'Geef de menu item code op indien u deze wilt benaderen via de API.',
+ 'static_page' => 'Statische pagina',
+ 'all_static_pages' => 'Alle statische pagina\'s'
+ ],
+ 'content' => [
+ 'menu_label' => 'Inhoud',
+ 'cant_save_to_dir' => 'Het is niet toegelaten bestanden met inhoud op te slaan in de static-pages map.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Toevoegen',
+ 'search' => 'Zoeken...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Onbekend object type',
+ 'not_found' => 'Het gevraagde object is niet gevonden.',
+ ],
+ 'editor' => [
+ 'title' => 'Titel',
+ 'new_title' => 'Nieuwe pagina titel',
+ 'content' => 'Inhoud',
+ 'url' => 'URL',
+ 'filename' => 'Bestandsnaam',
+ 'layout' => 'Layout',
+ 'description' => 'Beschrijving',
+ 'preview' => 'Voorbeeld',
+ 'enter_fullscreen' => 'Volledig scherm openen',
+ 'exit_fullscreen' => 'Volledig scherm afsluiten',
+ 'hidden' => 'Verborgen',
+ 'hidden_comment' => 'Verborgen pagina\'s zijn alleen toegankelijk voor ingelogde gebruikers.',
+ 'navigation_hidden' => 'Verbergen in de navigatie',
+ 'navigation_hidden_comment' => 'Indien aangevinkt, zal deze pagina niet weergegeven worden in automatisch gegenereerde menu\'s en kruimelpaden (breadcrumbs).',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Blokken',
+ 'code' => 'Blok code',
+ 'code_comment' => 'Voer een code in om dit fragment beschikbaar the maken in de Static Pages plugin.',
+ 'name' => 'Naam',
+ 'name_comment' => 'De naam wordt weergegeven in de blokkenlijst in de zijbalk en op de pagina waar dit blok wordt toegevoegd.',
+ 'no_records' => 'Geen blokken gevonden',
+ 'menu_label' => 'Blokken',
+ 'column_property' => 'Eigenschap naam',
+ 'column_type' => 'Type',
+ 'column_code' => 'Code',
+ 'column_default' => 'Standaardwaarde',
+ 'column_options' => 'Opties',
+ 'column_type_string' => 'Tekst',
+ 'column_type_checkbox' => 'Selectieveld',
+ 'column_type_dropdown' => 'Selectielijst',
+ 'not_found' => 'Blok met de gevraagde code :code is niet gevonden in het thema.',
+ 'property_format_error' => 'De naam van de eigenschap moet beginnen met een letter en kan alleen letters en cijfers bevatten.',
+ 'invalid_option_key' => 'Ongeldige selectlijst waarde: %s. Deze waarden mogen alleen cijfers, letters of de karakters _- bevatten.',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/pl/lang.php b/plugins/rainlab/pages/lang/pl/lang.php
new file mode 100644
index 000000000..b10c968c0
--- /dev/null
+++ b/plugins/rainlab/pages/lang/pl/lang.php
@@ -0,0 +1,142 @@
+ [
+ 'name' => 'Strony',
+ 'description' => 'Strony statyczne oraz menu.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Strony',
+ 'template_title' => '%s Strony',
+ 'delete_confirmation' => 'Czy na pewno chcesz usunąć wybrane strony? Podstrony również zostaną usnięte.',
+ 'no_records' => 'Nie znaleziono stron',
+ 'delete_confirm_single' => 'Czy na pewno chcesz usunąć stronę? Podstrony również zostaną usnięte.',
+ 'new' => 'Nowa strona',
+ 'add_subpage' => 'Dodaj podstronę',
+ 'invalid_url' => 'Niewprawidłowy format URL lub niedozwolone znaki.',
+ 'url_not_unique' => 'Podany URL istnieje już w bazie.',
+ 'layout' => 'Układ',
+ 'layouts_not_found' => 'Brak układów',
+ 'saved' => 'Strona została zapisana poprawnie.',
+ 'tab' => 'Strony',
+ 'manage_pages' => 'Zarządzaj stronami statycznymi',
+ 'manage_menus' => 'Zarządzaj menu statycznymi',
+ 'access_snippets' => 'Fragmenty',
+ 'manage_content' => 'Zarządzaj treścią statyczną',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menu',
+ 'delete_confirmation' => 'Czy na pewno chcesz usunąć wybrane menu?',
+ 'no_records' => 'Nie znaleziono menu',
+ 'new' => 'Nowe menu',
+ 'new_name' => 'Nowe menu',
+ 'new_code' => 'nowe-menu',
+ 'delete_confirm_single' => 'Czy na pewno chcesz usunąć wybrane menu?',
+ 'saved' => 'Menu zostało zapisane poprawnie.',
+ 'name' => 'Nazwa',
+ 'code' => 'Kod systemowy',
+ 'items' => 'Elementy menu',
+ 'add_subitem' => 'Dodaj element',
+ 'code_required' => 'Kod systemowy jest wymagany',
+ 'invalid_code' => 'Nieprawidłowy kod systemowy. Nie może zawierać znaków specjalnych',
+ ],
+ 'menuitem' => [
+ 'title' => 'Tytuł',
+ 'editor_title' => 'Edytuj element',
+ 'type' => 'Typ',
+ 'allow_nested_items' => 'Pozwól na zagnieżdżanie elementów',
+ 'allow_nested_items_comment' => 'Zagnieżdżone elementy mogą być utworzone dynamicznie np ze stron statycznych',
+ 'url' => 'URL',
+ 'reference' => 'Referencja',
+ 'search_placeholder' => 'Przeszukaj wszystkie referencje...',
+ 'title_required' => 'Tytuł jest wymagany',
+ 'unknown_type' => 'Nieznany typ elementu',
+ 'unnamed' => 'Brak nazwy typu elementu',
+ 'add_item' => 'Dodaj E lement',
+ 'new_item' => 'Nowy element menu',
+ 'replace' => 'Zamień element na elementy wenątrz tego elementu',
+ 'replace_comment' => 'Użyj tej opcji aby elementy znajdujące sie w tym elemencie były wygenerowane na poziomie tego elementu a sam element zostanie ukryty.',
+ 'cms_page' => 'Strona CMS',
+ 'cms_page_comment' => 'Wybierz stronę z cms do której ma kierować',
+ 'reference_required' => 'Referencja jest wymagana',
+ 'url_required' => 'URL jest wymagany',
+ 'cms_page_required' => 'Wybierz stronę z CMS',
+ 'display_tab' => 'Wyświetlanie',
+ 'hidden' => 'Ukryj',
+ 'hidden_comment' => 'Ukryj ten element menu na stronie',
+ 'attributes_tab' => 'Atrybuty',
+ 'code' => 'Kod systemowy',
+ 'code_comment' => 'Wprowadź kod systemowy aby móc go używać w API.',
+ 'css_class' => 'Klasa CSS',
+ 'css_class_comment' => 'Wprowadź klasę CSS, aby nadać temu elementowi niestandardowy wygląd',
+ 'external_link' => 'Zewnętrzny link',
+ 'external_link_comment' => 'Adres linku zostanie otworzony w nowym oknie',
+ 'static_page' => 'Strona statyczna',
+ 'all_static_pages' => 'Wszystkie strony statyczne',
+ ],
+ 'content' => [
+ 'menu_label' => 'Treść',
+ 'cant_save_to_dir' => 'Zapis treści jest niemożliwy. Sprawdź dostęp do folderu treści statycznych.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Dodaj',
+ 'search' => 'Szukaj...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Nieznany typ obiektu',
+ 'not_found' => 'Nie znaleziono objektu.',
+ ],
+ 'editor' => [
+ 'title' => 'Tytuł',
+ 'new_title' => 'Nowy tytuł strony',
+ 'content' => 'Treść',
+ 'url' => 'URL',
+ 'filename' => 'Nazwa pliku',
+ 'layout' => 'Układ',
+ 'description' => 'Opis',
+ 'preview' => 'Podgląd',
+ 'enter_fullscreen' => 'Pełny ekran',
+ 'exit_fullscreen' => 'Zamknij pełny ekran',
+ 'hidden' => 'Ukryta',
+ 'hidden_comment' => 'Ukryte strony są dostępne tylko dla zalogowanych administratorów.',
+ 'navigation_hidden' => 'Ukryj stronę w nawigacji',
+ 'navigation_hidden_comment' => 'Zaznacz aby usunąć strone z automatycznego generowania menu oraz ścieżek (breadcrumbs).',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Fragment',
+ 'code' => 'Kod systemowy',
+ 'code_comment' => 'Dodaj kod systemowy aby ten fragment był dostępny do użycia w treści stron statycznych.',
+ 'name' => 'Nazwa',
+ 'name_comment' => 'Nazwa będzie się wyświetlać w menu obok stron statycznych.',
+ 'no_records' => 'Nie znaleziono',
+ 'menu_label' => 'Fragmenty',
+ 'column_property' => 'Nazwa parametru',
+ 'column_type' => 'Typ',
+ 'column_code' => 'Kod systemowy',
+ 'column_default' => 'Domyślny',
+ 'column_options' => 'Opcje',
+ 'column_type_string' => 'Tekst',
+ 'column_type_checkbox' => 'Checkbox',
+ 'column_type_dropdown' => 'Lista rozwijana',
+ 'not_found' => 'Fragment o podanym kodzie systemowym :code nie został znaleziony.',
+ 'property_format_error' => 'Nazwa parametru musi się zaczynać od litery i może zawierać tylko litery oraz liczby',
+ 'invalid_option_key' => 'Nieprawidłowa opcja: :key. Opcja wyboru może zawierać tylko litery, cyfry, myślnik i podkreślnik',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Strona statyczna',
+ 'static_page_description' => 'Dodaje zawartość statycznej strony.',
+ 'static_page_use_content_name' => 'Użyj pola zawartości strony',
+ 'static_page_use_content_description' => 'Jeżeli odznaczone, sekcja treści nie pojawi się podczas edycji strony statycznej. Zawartość strony będzie ustalana wyłącznie na podstawie symboli zastępczych i zmiennych',
+ 'static_page_default_name' => 'Domyślny układ',
+ 'static_page_default_description' => 'Definiuje ten układ jako domyślny dla nowych stron',
+ 'static_page_child_layout_name' => 'Układ podstrony',
+ 'static_page_child_layout_description' => 'Układ, który będzie używany jako domyślny dla każdej nowej podstrony',
+ 'static_menu_name' => 'Menu',
+ 'static_menu_description' => 'Dodaje menu do strony.',
+ 'static_menu_code_name' => 'Menu',
+ 'static_menu_code_description' => 'Podaj kod systemowy menu, które ma zostać wyświetlone.',
+ 'static_breadcrumbs_name' => 'Okruszki (Breadcrumbs)',
+ 'static_breadcrumbs_description' => 'Zwraca ścieżkę stron statycznych.',
+ 'child_pages_name' => 'Strony podrzędne',
+ 'child_pages_description' => 'Wyświetla listę stron podrzędnych dla aktualnej strony',
+ 'static_menu_menu_code' => 'Podaj kod systemowy menu',
+ ],
+];
\ No newline at end of file
diff --git a/plugins/rainlab/pages/lang/pt-br/lang.php b/plugins/rainlab/pages/lang/pt-br/lang.php
new file mode 100644
index 000000000..03b20d16f
--- /dev/null
+++ b/plugins/rainlab/pages/lang/pt-br/lang.php
@@ -0,0 +1,115 @@
+ [
+ 'name' => 'Páginas',
+ 'description' => 'Gerenciar páginas e menus.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Páginas',
+ 'template_title' => '%s Páginas',
+ 'delete_confirmation' => 'Tem certeza que deseja excluir as páginas selecionadas? Todas as subpáginas também serão excluídas.',
+ 'no_records' => 'Nenhuma página encontrada',
+ 'delete_confirm_single' => 'Tem certeza que deseja excluir a página selecionada? Todas as subpáginas também serão excluídas.',
+ 'new' => 'Nova página',
+ 'add_subpage' => 'Adicionar subpágina',
+ 'invalid_url' => 'Formato inválido de URL. A URL deve iniciar com o símbolo / e pode conter apenas dígitos, letras latinas e os seguintes símbolos: _-/.',
+ 'url_not_unique' => 'Esta URL já está sendo utilizada por outra página.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Nenhum layout encontrado',
+ 'saved' => 'Página salva com sucesso.',
+ 'tab' => 'Páginas',
+ 'manage_pages' => 'Gerenciar páginas estáticas',
+ 'manage_menus' => 'Gerenciar menus estáticos',
+ 'access_snippets' => 'Acessar fragmentos',
+ 'manage_content' => 'Gerenciar conteúdos estáticos'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menus',
+ 'delete_confirmation' => 'Tem certeza que deseja excluir os menus selecionados?',
+ 'no_records' => 'Nenhum menu encontrado',
+ 'new' => 'Novo menu',
+ 'new_name' => 'Novo menu',
+ 'new_code' => 'novo-menu',
+ 'delete_confirm_single' => 'Tem certeza que deseja excluir este menu?',
+ 'saved' => 'Menu salvo com sucesso.',
+ 'name' => 'Nome',
+ 'code' => 'Código',
+ 'items' => 'Itens do menu',
+ 'add_subitem' => 'Adicionar subitem',
+ 'no_records' => 'Nenhum item encontrado',
+ 'code_required' => 'O código é necessário',
+ 'invalid_code' => 'Formato inválido de código. O código pode conter dígitos, letras latinas e os seguintes símbolos: _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Título',
+ 'editor_title' => 'Editar item',
+ 'type' => 'Tipo',
+ 'allow_nested_items' => 'Permitir itens aninhados',
+ 'allow_nested_items_comment' => 'Itens aninhados podem ser gerados dinamicamente por páginas estáticas e outros tipos de itens',
+ 'url' => 'URL',
+ 'reference' => 'Referência',
+ 'title_required' => 'O título é necessário',
+ 'unknown_type' => 'Tipo de item desconhecido',
+ 'unnamed' => 'Item de menu sem nome',
+ 'add_item' => 'Adicionar i tem',
+ 'new_item' => 'Novo item',
+ 'replace' => 'Substituir este item com seus filhos gerados',
+ 'replace_comment' => 'Use esta opção para empurrar os itens de menu gerados para o mesmo nível que este item. Este item em si será ocultado.',
+ 'cms_page' => 'Página CMS',
+ 'cms_page_comment' => 'Selecione uma página para abrir quando o item for clicado.',
+ 'reference_required' => 'A referência do item é necessária.',
+ 'url_required' => 'A URL é necessária',
+ 'cms_page_required' => 'Por favor, selecione uma página CMS',
+ 'code' => 'Código',
+ 'code_comment' => 'Entre com o código do item se deseja acessar com a API.'
+ ],
+ 'content' => [
+ 'menu_label' => 'Conteúdo',
+ 'cant_save_to_dir' => 'Não é permitido salvar arquivos de conteúdo no diretório de páginas estáticas.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Adicionar',
+ 'search' => 'Buscar...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Tipo de objeto desconhecido',
+ 'not_found' => 'O objeto requisitado não foi encontrado.'
+ ],
+ 'editor' => [
+ 'title' => 'Título',
+ 'new_title' => 'Título da nova página',
+ 'content' => 'Conteúdo',
+ 'url' => 'URL',
+ 'filename' => 'Nome do arquivo',
+ 'layout' => 'Layout',
+ 'description' => 'Descrição',
+ 'preview' => 'Visualizar',
+ 'enter_fullscreen' => 'Entrar no modo tela cheia',
+ 'exit_fullscreen' => 'Sair do modo tela cheia',
+ 'hidden' => 'Ocultar',
+ 'hidden_comment' => 'Páginas ocultas são acessíveis apenas para administradores.',
+ 'navigation_hidden' => 'Ocultar na navegação',
+ 'navigation_hidden_comment' => 'Marque esta opção para ocultar esta página de menus gerados automaticamente e itens de hierarquia de navegação.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Fragmentos',
+ 'code' => 'Código do fragmento',
+ 'code_comment' => 'Entre com o código para disponibilizar este bloco como um fragmento nas páginas estáticas.',
+ 'name' => 'Nome',
+ 'name_comment' => 'O nome é exibido na lista de fragmentos no menu lateral na área de páginas estáticas e nas páginas em que o fragmento é adicionado.',
+ 'no_records' => 'Nenhum fragmento encontrado',
+ 'menu_label' => 'Fragmentos',
+ 'column_property' => 'Título da propriedade',
+ 'column_type' => 'Tipo',
+ 'column_code' => 'Código',
+ 'column_default' => 'Padrão',
+ 'column_options' => 'Opções',
+ 'column_type_string' => 'Texto',
+ 'column_type_checkbox' => 'Caixa de seleção',
+ 'column_type_dropdown' => 'Caixa de seleção suspensa',
+ 'not_found' => 'Fragmento com o código :code não foi encontrado.',
+ 'property_format_error' => 'Código da propriedade deve iniciar com uma letra latina e pode conter apenas letras latinas e dígitos',
+ 'invalid_option_key' => 'Chave de opção inválida: %s. Chaves de opção podem conter apenas dígitos, letras latinas e os caracteres _ e -'
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/ru/lang.php b/plugins/rainlab/pages/lang/ru/lang.php
new file mode 100644
index 000000000..e3e9871ee
--- /dev/null
+++ b/plugins/rainlab/pages/lang/ru/lang.php
@@ -0,0 +1,118 @@
+ [
+ 'name' => 'Страницы',
+ 'description' => 'Страницы и меню.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Страницы',
+ 'template_title' => '%s Страницы',
+ 'delete_confirmation' => 'Вы действительно хотите удалить выбранные страницы? Это также удалит имеющиеся подстраницы.',
+ 'no_records' => 'Страниц не найдено',
+ 'delete_confirm_single' => 'Вы действительно хотите удалить эту страницу? Это также удалит имеющиеся подстраницы.',
+ 'new' => 'Новая страница',
+ 'add_subpage' => 'Добавить подстраницу',
+ 'invalid_url' => 'Некорректный формат URL. URL должен начинаться с прямого слеша и может содержать цифры, латинские буквы и следующие символы: _-/.',
+ 'url_not_unique' => 'Это URL уже используется другой страницей.',
+ 'layout' => 'Шаблон',
+ 'layouts_not_found' => 'Шаблоны не найдены',
+ 'saved' => 'Страница была успешно сохранена.',
+ 'tab' => 'Страницы',
+ 'manage_pages' => 'Управление страницами',
+ 'manage_menus' => 'Управление меню',
+ 'access_snippets' => 'Доступ к сниппетами',
+ 'manage_content' => 'Управление содержимым',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Меню',
+ 'delete_confirmation' => 'Вы действительно хотите удалить выбранные пункты меню?',
+ 'no_records' => 'Меню не найдены',
+ 'new' => 'Новое меню',
+ 'new_name' => 'Новое меню',
+ 'new_code' => 'novoe-menyu',
+ 'delete_confirm_single' => 'Вы действительно хотите удалить это меню?',
+ 'saved' => 'Меню было успешно сохранено.',
+ 'name' => 'Имя',
+ 'code' => 'Код',
+ 'items' => 'Пункты меню',
+ 'add_subitem' => 'Добавить подменю',
+ 'code_required' => 'Поле Код обязательно',
+ 'invalid_code' => 'Некорректный формат Кода. Код может содержать цифры, латинские буквы и следующие символы: _-/',
+ ],
+ 'menuitem' => [
+ 'title' => 'Название',
+ 'editor_title' => 'Редактировать пункт меню',
+ 'type' => 'Тип',
+ 'allow_nested_items' => 'Разрешить вложенные',
+ 'allow_nested_items_comment' => 'Вложенные пункты могут быть динамически сгенерированы статической страницей или другими типами элементов',
+ 'url' => 'URL',
+ 'reference' => 'Ссылка',
+ 'title_required' => 'Название обязательно',
+ 'unknown_type' => 'Неизвестный тип меню',
+ 'unnamed' => 'Безымянный пункт',
+ 'add_item' => 'Добавить пункт (i )',
+ 'new_item' => 'Новый пункт',
+ 'replace' => 'Заменять этот пункт его сгенерированными потомками',
+ 'replace_comment' => 'Отметьте для переноса генерируемых пунктов меню на один уровень с этим пунктом. Сам этот пункт будет скрыт.',
+ 'cms_page' => 'Страницы CMS',
+ 'cms_page_comment' => 'Выберите открываемую по клику страницу.',
+ 'reference_required' => 'Необходима ссылка для пункта меню.',
+ 'url_required' => 'Необходим URL',
+ 'cms_page_required' => 'Пожалуйста, выберите страницу CMS',
+ 'code' => 'Код',
+ 'code_comment' => 'Введите код пункта меню, если хотите иметь к нему доступ через API.',
+ ],
+ 'content' => [
+ 'menu_label' => 'Содержимое',
+ 'cant_save_to_dir' => 'Сохранение файлов содержимого в директорию static-pages запрещено.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Добавить',
+ 'search' => 'Поиск...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Неизвестный тип объекта',
+ 'not_found' => 'Запрашиваемый объект не найден.',
+ ],
+ 'editor' => [
+ 'title' => 'Название',
+ 'new_title' => 'Название новой страницы',
+ 'content' => 'Содержимое',
+ 'url' => 'URL',
+ 'filename' => 'Имя Файла',
+ 'layout' => 'Шаблон',
+ 'description' => 'Описание',
+ 'preview' => 'Предпросмотр',
+ 'enter_fullscreen' => 'Войти в полноэкранный режим',
+ 'exit_fullscreen' => 'Выйти из полноэкранного режима',
+ 'hidden' => 'Скрытый',
+ 'hidden_comment' => 'Скрытые страницы доступны только вошедшим администраторам.',
+ 'navigation_hidden' => 'Спрятать в навигации',
+ 'navigation_hidden_comment' => 'Отметьте, чтобы скрыть эту страницу в генерируемых меню и хлебных крошках.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Сниппеты',
+ 'code' => 'Код сниппета',
+ 'code_comment' => 'Введите код, чтобы сделать этот фрагмент доступным как сниппет в расширении Страницы.',
+ 'name' => 'Имя',
+ 'name_comment' => 'Имя отображается в списке сниппетов расширения Страницы и на странице, когда сниппет добавлен.',
+ 'no_records' => 'Сниппеты не найдены',
+ 'menu_label' => 'Сниппеты',
+ 'column_property' => 'Название свойства',
+ 'column_type' => 'Тип',
+ 'column_code' => 'Код',
+ 'column_default' => 'По умолчанию',
+ 'column_options' => 'Опции',
+ 'column_type_string' => 'Строка',
+ 'column_type_checkbox' => 'Чекбокс',
+ 'column_type_dropdown' => 'Выпадающий список',
+ 'not_found' => 'Сниппет с запрошенным кодом %s не найден в теме.',
+ 'property_format_error' => 'Код свойства должен начинаться с латинской буквы и может содержать только латинские буквы и цифры',
+ 'invalid_option_key' => 'Некорректный ключ выпадающего списка: %s. Ключ может содержать только цифры, латинские буквы и символы _ и -',
+ ],
+ 'component' => [
+ 'static_page_description' => 'Выводит страницу в CMS шаблоне.',
+ 'static_menu_description' => 'Выводит меню в CMS шаблоне.',
+ 'static_menu_menu_code' => 'Укажите код меню, которое должно быть показано',
+ 'static_breadcrumbs_description' => 'Выводит хлебные крошки для страницы.',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/sk/lang.php b/plugins/rainlab/pages/lang/sk/lang.php
new file mode 100644
index 000000000..8bfeb2a67
--- /dev/null
+++ b/plugins/rainlab/pages/lang/sk/lang.php
@@ -0,0 +1,141 @@
+ [
+ 'name' => 'Stránky',
+ 'description' => 'Funkcie pre správu stránok a menu.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Stránky',
+ 'template_title' => '%s Stránky',
+ 'delete_confirmation' => 'Naozaj chcete odstrániť vybrané stránky? Ak existujú nejaké podstránky, budú taktiež odstránené.',
+ 'no_records' => 'Neboli nájdené žiadne stránky',
+ 'delete_confirm_single' => 'Naozaj chcete odstrániť túto stránku? Ak existujú nejaké podstránky, budú taktiež odstránené.',
+ 'new' => 'Nová stránka',
+ 'add_subpage' => 'Pridať podstránku',
+ 'invalid_url' => 'Neplatný formát URL adresy. URL by mala začínať symbolom lomítka a môže obsahovať číslice, latinské písmená a nasledujúce znaky: _- /.',
+ 'url_not_unique' => 'Túto URL adresu už používa iná stránka.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Žiadne layouty neboli nájdené',
+ 'saved' => 'Stránka bola úspešne uložená.',
+ 'tab' => 'Stránky',
+ 'manage_pages' => 'Správa stránok',
+ 'manage_menus' => 'Správa menu',
+ 'access_snippets' => 'Správa snippetov',
+ 'manage_content' => 'Správa obsahu',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menu',
+ 'delete_confirmation' => 'Naozaj chcete odstrániť vybrané menu?',
+ 'no_records' => 'Neboli nájdené žiadne položky',
+ 'new' => 'Nové menu',
+ 'new_name' => 'Nové menu',
+ 'new_code' => 'nove-menu',
+ 'delete_confirm_single' => 'Naozaj chcete odstrániť toto menu?',
+ 'saved' => 'Menu bolo úspešne uložené',
+ 'name' => 'Názov',
+ 'code' => 'Kód',
+ 'items' => 'Položky menu',
+ 'add_subitem' => 'Pridať vnorenú položku',
+ 'code_required' => 'Pole kód je povinné.',
+ 'invalid_code' => 'Neplatný formát kódu. Kód môže obsahovať číslice, latinské písmená a nasledujúce znaky: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Názov',
+ 'editor_title' => 'Upraviť položku menu',
+ 'type' => 'Typ',
+ 'allow_nested_items' => 'Povoliť vnorené položky',
+ 'allow_nested_items_comment' => 'Vnorené položky môžu byť automaticky generované statickou stránkou alebo niektorými ďalšími typmi položiek',
+ 'url' => 'URL adresa',
+ 'reference' => 'Odkaz',
+ 'search_placeholder' => 'Prehľadať všetky odkazy...',
+ 'title_required' => 'Názov je povinný',
+ 'unknown_type' => 'Neznámy typ položky menu',
+ 'unnamed' => 'Nepomenovaná položka menu',
+ 'add_item' => 'Pridať P oložku',
+ 'new_item' => 'Nová položka menu',
+ 'replace' => 'Nahradiť túto položku jej generovanými vnorenými položkami',
+ 'replace_comment' => 'Zaškrtnite toto pole pokiaľ si prajete vnorené položky menu posunúť na rovnakú úroveň akú má táto položka. Samotná položka zostane skrytá.',
+ 'cms_page' => 'CMS stránka',
+ 'cms_page_comment' => 'Vyberte stránku, ktorá sa má otvoriť po kliknutí na položku v menu.',
+ 'reference_required' => 'Odkaz na položku menu je povinný.',
+ 'url_required' => 'Adresa URL je povinná',
+ 'cms_page_required' => 'Prosím vyberte CMS stránku',
+ 'display_tab' => 'Zobrazenie',
+ 'hidden' => 'Skrytá',
+ 'hidden_comment' => 'Skryť túto položku menu pre celú webovú stránku.',
+ 'attributes_tab' => 'Vlastnosti',
+ 'code' => 'Kód',
+ 'code_comment' => 'Zadajte kód položky menu ak k nej chcete pristupovať prostredníctvom API.',
+ 'css_class' => 'CSS trieda',
+ 'css_class_comment' => 'Zadajte názov CSS triedy, ktorá sa ma aplikovať pre túto položku menu.',
+ 'external_link' => 'Externý odkaz',
+ 'external_link_comment' => 'Otvoriť odkaz tejto položky menu v novom okne.',
+ 'static_page' => 'Statická stránka',
+ 'all_static_pages' => 'Všetky statické stránky'
+ ],
+ 'content' => [
+ 'menu_label' => 'Obsah',
+ 'cant_save_to_dir' => 'Ukladanie súborov s obsahom do adresára statických stránok nie je povolené.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Pridať',
+ 'search' => 'Hľadať...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Neznámy typ objektu',
+ 'not_found' => 'Požadovaný objekt nebol nájdený.',
+ ],
+ 'editor' => [
+ 'title' => 'Názov',
+ 'new_title' => 'Názov novej stránky',
+ 'content' => 'Obsah',
+ 'url' => 'URL adresa',
+ 'filename' => 'Názov súboru',
+ 'layout' => 'Layout',
+ 'description' => 'Popis',
+ 'preview' => 'Náhľad',
+ 'enter_fullscreen' => 'Zapnúť režim celej obrazovky',
+ 'exit_fullscreen' => 'Vypnúť režim celej obrazovky',
+ 'hidden' => 'Skrytá',
+ 'hidden_comment' => 'Skryté stránky sú prístupné iba prihláseným používateľom.',
+ 'navigation_hidden' => 'Skryť v menu',
+ 'navigation_hidden_comment' => 'Začiarknutím tohto poľa skryjete túto stránku z automaticky generovaných menu a navigačnej cesty.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Kód snippetu',
+ 'code_comment' => 'Zadajte kód, aby táto čiastková šablóna bola dostupná ako snippet v plugine statických stránok.',
+ 'name' => 'Názov',
+ 'name_comment' => 'Tento názov sa zobrazí v zozname snippetov v bočnom menu pluginu statických stránok a priamo na stránke, keď bude snippet pridaný.',
+ 'no_records' => 'Neboli nájdené žiadne snippety',
+ 'menu_label' => 'Snippety',
+ 'column_property' => 'Názov vlastnosti',
+ 'column_type' => 'Typ',
+ 'column_code' => 'Kód',
+ 'column_default' => 'Predvolená hodnota',
+ 'column_options' => 'Možnosti',
+ 'column_type_string' => 'Reťazec',
+ 'column_type_checkbox' => 'Zaškrtávacie pole',
+ 'column_type_dropdown' => 'Rozbaľovací zoznam',
+ 'not_found' => 'Snippet s požadovaným kódom :code nebol nájdený v téme.',
+ 'property_format_error' => 'Kód vlastnosti by mal začínať latinským písmenom a môže obsahovať len latinské písmená a číslice',
+ 'invalid_option_key' => 'Neplatný kľúč položky rozbaľovacieho zoznamu: :key. Kľúč položky rozbaľovacieho zoznamu môže obsahovať iba písmená, číslice a znaky: _-',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Statická stránka',
+ 'static_page_description' => 'Zobrazí obsah statickej stránky',
+ 'static_page_use_content_name' => 'Použiť pole obsah stránky',
+ 'static_page_use_content_description' => 'Ak nie je začiarknuté, sekcia obsahu sa nezobrazí pri úprave statickej stránky. Obsah stránky bude určený výhradne prostredníctvom zástupcov a premenných.',
+ 'static_page_default_name' => 'Predvolený layout',
+ 'static_page_default_description' => 'Nastaví tento layout ako predvolený pre nové stránky',
+ 'static_page_child_layout_name' => 'Layout podstránky',
+ 'static_page_child_layout_description' => 'Layout ktorý sa má použiť ako predvolený pre všetky nové podstránky',
+ 'static_menu_name' => 'Statické menu',
+ 'static_menu_description' => 'Zobrazí menu na stránke',
+ 'static_menu_code_name' => 'Menu',
+ 'static_menu_code_description' => 'Zadajte kód menu, ktoré má komponent zobraziť.',
+ 'static_breadcrumbs_name' => 'Statická navigačná cesta',
+ 'static_breadcrumbs_description' => 'Zobrazí navigačnú cestu na stránke.',
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/sl/lang.php b/plugins/rainlab/pages/lang/sl/lang.php
new file mode 100644
index 000000000..d15cc0e7a
--- /dev/null
+++ b/plugins/rainlab/pages/lang/sl/lang.php
@@ -0,0 +1,145 @@
+ [
+ 'name' => 'Strani',
+ 'description' => 'Ustvarjanje strani in menijev.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Strani',
+ 'template_title' => '%s strani',
+ 'delete_confirmation' => 'Ali ste prepričani, da želite izbrisati izbrane strani? S tem boste izbrisali tudi njihove podstrani, če obstajajo.',
+ 'no_records' => 'Ni najdenih strani.',
+ 'delete_confirm_single' => 'Ali ste prepričani, da želite izbrisati to stran? S tem boste izbrisali tudi njene podstrani, če obstajajo.',
+ 'new' => 'Nova stran',
+ 'add_subpage' => 'Dodaj podstran',
+ 'invalid_url' => 'Neveljavna oblika URL formata. URL se mora začeti z znakom za desno poševnico in lahko vsebuje številke, latinične črke in naslednje znake: _-/.',
+ 'url_not_unique' => 'To URL povezavo uporablja že ena od drugih strani.',
+ 'layout' => 'Postavitev',
+ 'layouts_not_found' => 'Ni najdenih postavitev.',
+ 'saved' => 'Stran je bila uspešno shranjena.',
+ 'tab' => 'Strani',
+ 'manage_pages' => 'Upravljanje statičnih strani',
+ 'manage_menus' => 'Upravljanje statičnih menijev',
+ 'access_snippets' => 'Dostop do gradnikov',
+ 'manage_content' => 'Upravljanje statičnih vsebin',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Meniji',
+ 'delete_confirmation' => 'Ali ste prepričani, da želite izbrisati izbrane menije?',
+ 'no_records' => 'Ni najdenih menijev.',
+ 'new' => 'Nov meni',
+ 'new_name' => 'Nov meni',
+ 'new_code' => 'nov-meni',
+ 'delete_confirm_single' => 'Ali ste prepričani, da želite izbrisati ta meni?',
+ 'saved' => 'Meni je uspešno shranjen.',
+ 'name' => 'Ime',
+ 'code' => 'Koda',
+ 'items' => 'Elementi menija',
+ 'add_subitem' => 'Dodaj pod-element',
+ 'code_required' => 'Koda je obvezna.',
+ 'invalid_code' => 'Neveljaven format kode. Koda lahko vsebuje številke, latinične črke in naslednje znake: _-',
+ ],
+ 'menuitem' => [
+ 'title' => 'Naslov',
+ 'editor_title' => 'Element menija',
+ 'type' => 'Vrsta',
+ 'allow_nested_items' => 'Dovoli gnezdene elemente',
+ 'allow_nested_items_comment' => 'Gnezdene elemente lahko dinamično ustvarijo statične strani in nekatere druge vrste elementov.',
+ 'url' => 'URL',
+ 'reference' => 'Referenca',
+ 'search_placeholder' => 'Išči po vseh referencah...',
+ 'title_required' => 'Naslov je obvezen',
+ 'unknown_type' => 'Neznana vrsta elementa menija.',
+ 'unnamed' => 'Neimenovan element menija.',
+ 'add_item' => 'Dodaj element',
+ 'new_item' => 'Nov element menija',
+ 'replace' => 'Zamenjaj ta element z njegovimi pod-elementi',
+ 'replace_comment' => 'Z uporabo tega kvadratka lahko potisnete ustvarjene elemente menija na njegov nivo, ob tem pa bo le-ta element postal skrit.',
+ 'cms_page' => 'CMS stran',
+ 'cms_page_comment' => 'Izberite stran, ki naj se odpre ob kliku na element menija.',
+ 'reference_required' => 'Referenca elementa menija je obvezna.',
+ 'url_required' => 'Povezava URL je obvezna.',
+ 'cms_page_required' => 'Prosimo, izberite CMS stran.',
+ 'display_tab' => 'Prikaz',
+ 'hidden' => 'Skrito',
+ 'hidden_comment' => 'Element menija na spletni strani naj ne bo prikazan.',
+ 'attributes_tab' => 'Atributi',
+ 'code' => 'Koda',
+ 'code_comment' => 'Vnesite kodo za element menija, če želite do njega omogočiti API dostop.',
+ 'css_class' => 'CSS razred',
+ 'css_class_comment' => 'Vnesite ime CSS razreda, če želite elementu omogočiti videz po meri.',
+ 'external_link' => 'Zunanja povezava',
+ 'external_link_comment' => 'Povezava za ta element menija naj se odpre v novem oknu.',
+ 'static_page' => 'Statična stran',
+ 'all_static_pages' => 'Vse statične strani',
+ ],
+ 'content' => [
+ 'menu_label' => 'Vsebine',
+ 'saved' => 'Vsebina je bila uspešno shranjena.',
+ 'cant_save_to_dir' => 'Shranjevanje datotek z vsebino v mapo statičnih strani ni dovoljeno.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Dodaj',
+ 'search' => 'Išči...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Neznana vrsta objekta',
+ 'unauthorized_type' => 'Nimate pooblastil za upravljanje :type objektov.',
+ 'not_found' => 'Zahtevanega objekta ni mogoče najti.',
+ ],
+ 'editor' => [
+ 'title' => 'Naslov',
+ 'new_title' => 'Nov naslov strani',
+ 'content' => 'Vsebina',
+ 'url' => 'URL',
+ 'filename' => 'Ime datoteke',
+ 'layout' => 'Postavitev',
+ 'description' => 'Opis',
+ 'preview' => 'Predogled',
+ 'enter_fullscreen' => 'Celozaslonski način',
+ 'exit_fullscreen' => 'Zapri celozaslonski način',
+ 'hidden' => 'Skrita stran',
+ 'hidden_comment' => 'Skrite strani so dostopne le prijavljenim administratorjem.',
+ 'navigation_hidden' => 'Skrita v navigaciji',
+ 'navigation_hidden_comment' => 'Skrite strani v navigaciji se v menijih in povezavah ne prikažejo.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Gradniki',
+ 'code' => 'Koda gradnika',
+ 'code_comment' => 'Vnesite kodo, s katero bo ta predloga na voljo kot gradnik v vtičniku za statične strani.',
+ 'name' => 'Ime',
+ 'name_comment' => 'Ime se prikaže na seznamu gradnikov, na stranskem meniju statičnih strani in na vsebini strani, na katero je dodan gradnik.',
+ 'no_records' => 'Ni najdenih gradnikov.',
+ 'menu_label' => 'Gradniki',
+ 'column_property' => 'Naslov lastnosti',
+ 'column_type' => 'Tip',
+ 'column_code' => 'Koda',
+ 'column_default' => 'Privzeto',
+ 'column_options' => 'Možnosti',
+ 'column_type_string' => 'Niz znakov',
+ 'column_type_checkbox' => 'Potrditveno polje',
+ 'column_type_dropdown' => 'Spustni meni',
+ 'not_found' => 'Gradnika s kodo :code v trenutni temi ni mogoče najti.',
+ 'property_format_error' => 'Koda lastnosti se mora začeti z latinično črko in lahko vsebuje samo latinične črke in številke.',
+ 'invalid_option_key' => 'Neveljaven ključ elementa spustnega seznama: :key. Ključi lahko vsebujejo le številke, latinične črke in znaka _ ter -.',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Statična stran',
+ 'static_page_description' => 'Ustvari statično stran na CMS postavitvi.',
+ 'static_page_use_content_name' => 'Uporabi polje z vsebino strani',
+ 'static_page_use_content_description' => 'Če ni označeno, se razdelek z vsebino pri urejanju statične strani ne bo prikazal. Vsebina strani bo določena izključno prek vsebinskih okvirov in spremenljivk.',
+ 'static_page_default_name' => 'Privzeta postavitev',
+ 'static_page_default_description' => 'To postavitev definira kot privzeto za nove strani.',
+ 'static_page_child_layout_name' => 'Postavitev podstrani',
+ 'static_page_child_layout_description' => 'To postavitev definira kot privzeto za vse nove podstrani.',
+ 'static_menu_name' => 'Statični meni',
+ 'static_menu_description' => 'Ustvari meni na CMS postavitvi.',
+ 'static_menu_code_name' => 'Meni',
+ 'static_menu_code_description' => 'Določite kodo menija, ki ga mora sestaviti komponenta.',
+ 'static_breadcrumbs_name' => 'Statične povezave',
+ 'static_breadcrumbs_description' => 'Ustvari povezave za statično stran.',
+ 'child_pages_name' => 'Podstrani',
+ 'child_pages_description' => 'Prikaže seznam podstrani za trenutno stran.',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/sv/lang.php b/plugins/rainlab/pages/lang/sv/lang.php
new file mode 100644
index 000000000..8e09e331a
--- /dev/null
+++ b/plugins/rainlab/pages/lang/sv/lang.php
@@ -0,0 +1,115 @@
+ [
+ 'name' => 'Sidor',
+ 'description' => 'Sidor & menyer.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Sidor',
+ 'template_title' => '%s Sidor',
+ 'delete_confirmation' => 'Vill du verkligen ta bort de valda sidorna? Detta kommer också ta bort undersidorna, ifall det finns några.',
+ 'no_records' => 'Inga sidor hittades',
+ 'delete_confirm_single' => 'Vill du verkligen ta bort den valda sidan? Detta kommer också ta bort sidans undersidor, ifall det finns några.',
+ 'new' => 'Ny sida',
+ 'add_subpage' => 'Lägg till undersida',
+ 'invalid_url' => 'Ogiltigt format på URL. URL:en ska börja med slash och kan innehålla siffror, latinska bokstäver och följande symboler: _-/',
+ 'url_not_unique' => 'Denna URL används redan av en annan sida.',
+ 'layout' => 'Layout',
+ 'layouts_not_found' => 'Layouter kan inte hittas',
+ 'saved' => 'Sidan har sparats.',
+ 'tab' => 'Sidor',
+ 'manage_pages' => 'Hantera statiska sidor',
+ 'manage_menus' => 'Hantera statiska menyer',
+ 'access_snippets' => 'Hantera stumpar',
+ 'manage_content' => 'Hantera statiskt innehåll'
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menyer',
+ 'delete_confirmation' => 'Vill du verkligen ta bort valda de menyerna?',
+ 'no_records' => 'Inga menyer kunde finnas',
+ 'new' => 'Ny meny',
+ 'new_name' => 'Ny meny',
+ 'new_code' => 'ny-meny',
+ 'delete_confirm_single' => 'Vill du verkligen ta bort denna menyn?',
+ 'saved' => 'Menyn har sparats.',
+ 'name' => 'Namn',
+ 'code' => 'Kod',
+ 'items' => 'Menyföremål',
+ 'add_subitem' => 'Lägg till underföremål',
+ 'no_records' => 'Inga föremål kunde finnas',
+ 'code_required' => 'Koden är obligatorisk',
+ 'invalid_code' => 'Ogiltigt kodformat. Koden kan innehålla siffror, latinska bokstäver och följande symboler: _-'
+ ],
+ 'menuitem' => [
+ 'title' => 'Rubrik',
+ 'editor_title' => 'Redigera menyföremål',
+ 'type' => 'Typ',
+ 'allow_nested_items' => 'Tillåt underliggande föremål',
+ 'allow_nested_items_comment' => 'Underliggande föremål kan skapas dynamiskt av en etatisk sida och några andra föremålstyper',
+ 'url' => 'URL',
+ 'reference' => 'Referens',
+ 'title_required' => 'Rubriken är obligatorisk',
+ 'unknown_type' => 'Okänd menyföremålstyp',
+ 'unnamed' => 'Namnlöst menyföremål',
+ 'add_item' => 'Lägg till f öremål',
+ 'new_item' => 'Nytt menyföremål',
+ 'replace' => 'Ersätt detta föremål med dens skapade underföremål',
+ 'replace_comment' => 'Använd denna kryssruta för att föra skapade menyföremål till samma nivå som detta föremålet. Detta föremålet kommer att gömmas.',
+ 'cms_page' => 'CMS-sida',
+ 'cms_page_comment' => 'Välj en sida som ska öppnas när menyföremålet klickas.',
+ 'reference_required' => 'Menyföremålets referens är obligatorisk.',
+ 'url_required' => 'URL:en är obligatorisk',
+ 'cms_page_required' => 'Vänligen välj en CMS-sida',
+ 'code' => 'Kod',
+ 'code_comment' => 'Fyll i menyföremålets kod om du vill få tillgång till det i API:t.'
+ ],
+ 'content' => [
+ 'menu_label' => 'Innehåll',
+ 'cant_save_to_dir' => 'Att sparar innehållsfiler till mappen för statiska sidor är inte tillåtet.'
+ ],
+ 'sidebar' => [
+ 'add' => 'Lägg till',
+ 'search' => 'Sök...'
+ ],
+ 'object' => [
+ 'invalid_type' => 'Ogiltig objekttyp',
+ 'not_found' => 'Det begärda objektet kunde inte finnas.'
+ ],
+ 'editor' => [
+ 'title' => 'Rubrik',
+ 'new_title' => 'Ny sidrubrik',
+ 'content' => 'Innehåll',
+ 'url' => 'URL',
+ 'filename' => 'Filnamn',
+ 'layout' => 'Layout',
+ 'description' => 'Beskrivning',
+ 'preview' => 'Förhandsgranska',
+ 'enter_fullscreen' => 'Gå in i fullskärmsläge',
+ 'exit_fullscreen' => 'Gå ut ur fullskärmsläge',
+ 'hidden' => 'Gömd',
+ 'hidden_comment' => 'Gömda sidor är bara tillgängliga för inloggande back-endanvändare.',
+ 'navigation_hidden' => 'Göm i navigation',
+ 'navigation_hidden_comment' => 'Fyll i denna rutan för att gömma sidan från automatiskt skapade menyer och sökvägar.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Stump',
+ 'code' => 'Stumpkod',
+ 'code_comment' => 'Skriv in en kod som gör att denna sidurklipp är tillgänglig som en stump i tillägget för statiska sidor.',
+ 'name' => 'Namn',
+ 'name_comment' => 'Namnet visas i listan med stumpar i sidopanelen och på en sida när stumpen är tillagd.',
+ 'no_records' => 'Inga stumpar hittades',
+ 'menu_label' => 'Stumpar',
+ 'column_property' => 'Egenskapsrubrik',
+ 'column_type' => 'Typ',
+ 'column_code' => 'Kod',
+ 'column_default' => 'Standard',
+ 'column_options' => 'Alternativ',
+ 'column_type_string' => 'Sträng',
+ 'column_type_checkbox' => 'Kryssruta',
+ 'column_type_dropdown' => 'Rullgardinsmeny',
+ 'not_found' => 'En stump med den begärda koden :code kunde inte hittas i temat.',
+ 'property_format_error' => 'Egenskapskoden ska börja med en latisk bokstav kan bara innehålla latinska bokstäver samt siffror',
+ 'invalid_option_key' => 'Nyckeln: %s, i Rullgardinsmenyn är ogiltig. Alternativnycklarna kan bara innehålla siffror, latinska bokstäver och karaktärerna _ samt -'
+ ]
+];
diff --git a/plugins/rainlab/pages/lang/tr/lang.php b/plugins/rainlab/pages/lang/tr/lang.php
new file mode 100644
index 000000000..6f0be1268
--- /dev/null
+++ b/plugins/rainlab/pages/lang/tr/lang.php
@@ -0,0 +1,141 @@
+ [
+ 'name' => 'Sayfalar',
+ 'description' => 'Sayfalar & menüler modülü.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Sayfalar',
+ 'template_title' => '%s Sayfalar',
+ 'delete_confirmation' => 'Seçili sayfaları silmek istiyor musunuz? Alt sayfalar da silinecektir.',
+ 'no_records' => 'Sayfa bulunamadı',
+ 'delete_confirm_single' => 'Bu sayfayı silmek istiyor musunuz? Alt sayfalar da silinecektir',
+ 'new' => 'Yeni sayfa',
+ 'add_subpage' => 'Altsayfa ekle',
+ 'invalid_url' => 'Geçersiz URL formatı. URL eğik çizgi sembolü ile başlamalıdır ve rakam, latin harfleri ve bu sembolleri: _-/. içerebilir.',
+ 'url_not_unique' => 'Bu URL başka bir sayfa tarafından kullanılıyor',
+ 'layout' => 'Şablon',
+ 'layouts_not_found' => 'Şablon bulunamadı',
+ 'saved' => 'Sayfa başarıyla kaydedildi.',
+ 'tab' => 'Sayfalar',
+ 'manage_pages' => 'Sayfaları yönetebilsin',
+ 'manage_menus' => 'Menüleri yönetebilsin',
+ 'access_snippets' => 'Snippetleri yönetebilsin',
+ 'manage_content' => 'Sabit içerikleri yönetebilsin',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Menüler',
+ 'delete_confirmation' => 'Seçili menüleri silmek istiyor musunuz?',
+ 'no_records' => 'Menü bulunamadı',
+ 'new' => 'Yeni Menü',
+ 'new_name' => 'Yeni menü',
+ 'new_code' => 'yeni-menu',
+ 'delete_confirm_single' => 'Bu menüyü silmek istiyor musunuz?',
+ 'saved' => 'Menü başarıyla kaydedildi.',
+ 'name' => 'İsim',
+ 'code' => 'Kod',
+ 'items' => 'Menü Ögeleri',
+ 'add_subitem' => 'Altöge ekle',
+ 'code_required' => 'Kod gerekli',
+ 'invalid_code' => 'Geçersiz KOD formatı. Kod yalnızca rakam, Latin harfleri ve bu sembolleri: _- içerebilir.',
+ ],
+ 'menuitem' => [
+ 'title' => 'Başlık',
+ 'editor_title' => 'Menü Ögesini Düzenle',
+ 'type' => 'Tür',
+ 'allow_nested_items' => 'İçiçe ögelere izin ver',
+ 'allow_nested_items_comment' => 'İç içe öğeler statik sayfa ve bazı diğer öğe türlerine göre dinamik olarak üretilen olabilir',
+ 'url' => 'URL',
+ 'reference' => 'Referans',
+ 'search_placeholder' => 'Referansları ara...',
+ 'title_required' => 'Başlık gerekli',
+ 'unknown_type' => 'Geçersiz menü ögesi türü',
+ 'unnamed' => 'İsimsiz menü ögesi',
+ 'add_item' => 'Ö ge Ekle',
+ 'new_item' => 'Yeni menü ögesi',
+ 'replace' => 'Bu ögeyi oluşturulan çocuklarıyla değiştir',
+ 'replace_comment' => 'Use this checkbox to push generated menu items to the same level with this item. This item itself will be hidden.',
+ 'cms_page' => 'CMS Sayfası',
+ 'cms_page_comment' => 'Menü ögesine tıklandığında açılacak sayfayı seçin',
+ 'reference_required' => 'Menü ögesi referansı gereklidir.',
+ 'url_required' => 'URL gereklidir',
+ 'cms_page_required' => 'Lütfen bir CMS sayfası seçin',
+ 'display_tab' => 'Görünüm',
+ 'hidden' => 'Gizli',
+ 'hidden_comment' => 'Bu menüyü önyüzde gizle.',
+ 'attributes_tab' => 'Öznitellikler',
+ 'code' => 'Kod',
+ 'code_comment' => 'API ile giriş yapabilmek için menü ögesi kodunu girin.',
+ 'css_class' => 'CSS Class',
+ 'css_class_comment' => 'Bu menüye özel bir görünüm vermek için bir CSS sınıfı adı girin.',
+ 'external_link' => 'Dış link',
+ 'external_link_comment' => 'Bu menü için bağlantıları yeni sekmede aç.',
+ 'static_page' => 'Sayfa',
+ 'all_static_pages' => 'Tüm sayfalar',
+ ],
+ 'content' => [
+ 'menu_label' => 'İçerik',
+ 'cant_save_to_dir' => 'Statik sayfalar dizinine içerik dosyalarını kaydetme izni verilmez.',
+ ],
+ 'sidebar' => [
+ 'add' => 'Ekle',
+ 'search' => 'Ara...',
+ ],
+ 'object' => [
+ 'invalid_type' => 'Bilineyen nesne türü',
+ 'not_found' => 'İstenen nesne bulunamadı',
+ ],
+ 'editor' => [
+ 'title' => 'Başlık',
+ 'new_title' => 'Yeni sayfa başlığı',
+ 'content' => 'İçerik',
+ 'url' => 'URL',
+ 'filename' => 'Dosya Adı',
+ 'layout' => 'Layout',
+ 'description' => 'Tanımlama',
+ 'preview' => 'Önizleme',
+ 'enter_fullscreen' => 'Tam Ekran moduna geç',
+ 'exit_fullscreen' => 'Tam Ekran modundan çık',
+ 'hidden' => 'Gizli',
+ 'hidden_comment' => 'Gizli sayfalar yalnızca yönetim paneline giriş yapmış kullanıcılar tarafından görüntülenebilir.',
+ 'navigation_hidden' => 'Menüde Gizle',
+ 'navigation_hidden_comment' => 'Otomatik olarak oluşturulan menüler ve kırıntıları gizlemek için bu kutuyu işaretleyin.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Snippet kodu',
+ 'code_comment' => 'Sayfalar eklentisinde snippet olarak kullanabilmek için bir kod tanımlayın.',
+ 'name' => 'İsim',
+ 'name_comment' => 'Sol snippet listesinde görüntülenecek ismi girin.',
+ 'no_records' => 'Snippet bulunamadı',
+ 'menu_label' => 'Snippetlar',
+ 'column_property' => 'Property başlığı',
+ 'column_type' => 'Tip',
+ 'column_code' => 'Kod',
+ 'column_default' => 'Ön tanımlı (default)',
+ 'column_options' => 'Seçenekler (options)',
+ 'column_type_string' => 'Metin (string)',
+ 'column_type_checkbox' => 'Seçmeli (checkbox)',
+ 'column_type_dropdown' => 'Açılır liste (dropdown)',
+ 'not_found' => 'Tema için :code kodu ile istenilen snippet bulunamadı.',
+ 'property_format_error' => 'Kod sadece latin karakterle başlamalı ve latin karakter veya sayı içermelidir',
+ 'invalid_option_key' => 'Seçenek key i geçersiz: :key. Seçenek keyleri sadece sayı, Latin harfler ve karakter _ ve - içerebilir',
+ ],
+ 'component' => [
+ 'static_page_name' => 'Sabit sayfa',
+ 'static_page_description' => 'CMS bölümüne sabit sayfa içeriği ekler.',
+ 'static_page_use_content_name' => 'Sayfa içeriği alanını kullan',
+ 'static_page_use_content_description' => 'Seçilmezse, statik sayfa düzenlenirken içerik bölümü görünmez. Sayfa içeriği yalnızca placeholderlar ve değişkenler aracılığıyla belirlenir.',
+ 'static_page_default_name' => 'Varsayılan şablon',
+ 'static_page_default_description' => 'Bu şablonu yeni sayfalar için varsayılan olarak tanımlar.',
+ 'static_page_child_layout_name' => 'Alt sayfa şablobu',
+ 'static_page_child_layout_description' => 'Yeni alt sayfalar için varsayılan olarak kullanılacak şablon',
+ 'static_menu_name' => 'Statik (sabit) menü',
+ 'static_menu_description' => 'CMS bölümüne sabit menü içeriği ekler.',
+ 'static_menu_code_name' => 'Menü',
+ 'static_menu_code_description' => 'Component in göstereceği menünün kodunu belirtin.',
+ 'static_breadcrumbs_name' => 'Sabit breadcrumbs',
+ 'static_breadcrumbs_description' => 'Sabit sayfaya breadcrumbs ekler.',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/uk/lang.php b/plugins/rainlab/pages/lang/uk/lang.php
new file mode 100644
index 000000000..3434229fb
--- /dev/null
+++ b/plugins/rainlab/pages/lang/uk/lang.php
@@ -0,0 +1,118 @@
+ [
+ 'name' => 'Сторінки',
+ 'description' => 'Сторінки і меню.',
+ ],
+ 'page' => [
+ 'menu_label' => 'Сторінки',
+ 'template_title' => '%s Сторінки',
+ 'delete_confirmation' => 'Ви дійсно хочете видалити вибрані сторінки? Це також видалить наявні підсторінки.',
+ 'no_records' => 'Сторінок не знайдено',
+ 'delete_confirm_single' => 'Ви дійсно хочете видалити цю сторінку? Це також видалить наявні підсторінки.',
+ 'new' => 'Нова сторінка',
+ 'add_subpage' => 'Додати підсторінку',
+ 'invalid_url' => 'Некоректний формат URL. URL повинен починатися з прямого слеша і може містити цифри, латинські літери і такі символи: _-/.',
+ 'url_not_unique' => 'Цей URL вже використовується іншою сторінкою.',
+ 'layout' => 'Шаблон',
+ 'layouts_not_found' => 'Шаблони не знайдені',
+ 'saved' => 'Сторінка була успішно збережена.',
+ 'tab' => 'Сторінки',
+ 'manage_pages' => 'Управління сторінками',
+ 'manage_menus' => 'Управління меню',
+ 'access_snippets' => 'Доступ до сніппетів',
+ 'manage_content' => 'Управління змістом',
+ ],
+ 'menu' => [
+ 'menu_label' => 'Меню',
+ 'delete_confirmation' => 'Ви дійсно хочете видалити вибрані пункти меню?',
+ 'no_records' => 'Меню не знайдені',
+ 'new' => 'Нове меню',
+ 'new_name' => 'Новое меню',
+ 'new_code' => 'nove-menu',
+ 'delete_confirm_single' => 'Ви дійсно хочете видалити це меню?',
+ 'saved' => 'Меню було успішно збережено.',
+ 'name' => "Ім`я",
+ 'code' => 'Код',
+ 'items' => 'Пункти меню',
+ 'add_subitem' => 'Додати підменю',
+ 'code_required' => "Поле Код обов'язкове",
+ 'invalid_code' => 'Некоректний формат Коду. Код може містити цифри, латинські літери і такі символи: _-/',
+ ],
+ 'menuitem' => [
+ 'title' => 'Назва',
+ 'editor_title' => 'Редагувати пункт меню',
+ 'type' => 'Тип',
+ 'allow_nested_items' => 'Дозволити вкладені',
+ 'allow_nested_items_comment' => 'Вкладені пункти можуть бути динамічно згенеровані статичною сторінкою або іншими типами елементів',
+ 'url' => 'URL',
+ 'reference' => 'Посилання',
+ 'title_required' => "Назва обов'язково",
+ 'unknown_type' => 'Невідомий тип меню',
+ 'unnamed' => 'Безіменний пункт',
+ 'add_item' => 'Додати пункт ( i )',
+ 'new_item' => 'Новий пункт',
+ 'replace' => 'Замінювати цей пункт його згенеруванними нащадками',
+ 'replace_comment' => 'Відмітьте для перенесення згенерованних пунктів меню на один рівень з цим пунктом. Сам цей пункт буде приховано.',
+ 'cms_page' => 'Сторінки CMS',
+ 'cms_page_comment' => 'Оберіть відкриваючу при натисканні сторінку.',
+ 'reference_required' => 'Необхідне посилання для пункту меню.',
+ 'url_required' => 'Необхідний URL',
+ 'cms_page_required' => 'Будь ласка, виберіть сторінку CMS',
+ 'code' => 'Код',
+ 'code_comment' => 'Введіть код пункту меню, якщо хочете мати до нього доступ через API.',
+ ],
+ 'content' => [
+ 'menu_label' => 'Зміст',
+ 'cant_save_to_dir' => 'Збереження файлів змісту в директорію static-pages заборонено.',
+ ],
+ 'sidebar' => [
+ 'Add' => 'Додати',
+ 'search' => 'Пошук...',
+ ],
+ 'object' => [
+ 'invalid_type' => "Невідомий тип об'єкту",
+ 'not_found' => "Запитуваний об'єкт не знайдено.",
+ ],
+ 'editor' => [
+ 'title' => 'Назва',
+ 'new_title' => 'Назва нової сторінки',
+ 'content' => 'Зміст',
+ 'url' => 'URL',
+ 'filename' => "Ім'я файлу",
+ 'layout' => 'Шаблон',
+ 'description' => 'Опис',
+ 'preview' => 'Попередній огляд',
+ 'enter_fullscreen' => 'Увійти в повноекранний режим',
+ 'exit_fullscreen' => 'Вийти з повноекранного режиму',
+ 'hidden' => 'Прихований',
+ 'hidden_comment' => 'Приховані сторінки доступні тільки увійшовшим адміністраторам.',
+ 'navigation_hidden' => 'Сховати в навігації',
+ 'navigation_hidden_comment' => 'Відмітьте, щоб приховати цю сторінку в генеруючих меню і хлібних крихтах.',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Сніппети',
+ 'code' => 'Код сніппета',
+ 'code_comment' => 'Введіть код, щоб зробити цей фрагмент доступним як сніппет в розширенні Сторінки.',
+ 'name' => "Ім'я",
+ 'name_comment' => "Ім'я відображається в списку фрагментів розширення Сторінки і на сторінці, коли сніппет доданий.",
+ 'no_records' => 'Сніппети не знайдені',
+ 'menu_label' => 'Сніппети',
+ 'column_property' => 'Назва властивості',
+ 'column_type' => 'Тип',
+ 'column_code' => 'Код',
+ 'column_default' => 'За замовчуванням',
+ 'column_options' => 'Опції',
+ 'column_type_string' => 'Рядок',
+ 'column_type_checkbox' => 'Чекбокс',
+ 'column_type_dropdown' => 'Список, що випадає',
+ 'not_found' => 'Сніппет з запитаним кодом %s не знайдений в темі.',
+ 'property_format_error' => 'Код властивості повинен починатися з літери та може містити тільки латинські букви і цифри',
+ 'invalid_option_key' => 'Неправильний ключ списку: %s. Ключ може містити тільки цифри, латинські літери і символи _ і -',
+ ],
+ 'component' => [
+ 'static_page_description' => 'Виводити сторінку в CMS шаблоні.',
+ 'static_menu_description' => 'Виводити меню в CMS шаблоні.',
+ 'static_menu_menu_code' => 'Вкажіть код меню, яке повинно бути показано',
+ 'static_breadcrumbs_description' => 'Виводити хлібні крихти для сторінки.',
+ ],
+];
diff --git a/plugins/rainlab/pages/lang/zh-cn/lang.php b/plugins/rainlab/pages/lang/zh-cn/lang.php
new file mode 100644
index 000000000..0caa473b6
--- /dev/null
+++ b/plugins/rainlab/pages/lang/zh-cn/lang.php
@@ -0,0 +1,132 @@
+ [
+ 'name' => '页面',
+ 'description' => '页面和菜单功能拓展',
+ ],
+ 'page' => [
+ 'menu_label' => '页面',
+ 'template_title' => '%s 页面',
+ 'delete_confirmation' => '你真的要删除所选页面吗? 如果有子页面,也将被删除。',
+ 'no_records' => '找不到页面',
+ 'delete_confirm_single' => '你真的要删除这个页面吗? 如果有子页面,也将被删除。',
+ 'new' => '新建',
+ 'add_subpage' => '插入子页面',
+ 'invalid_url' => 'URL 格式无效,应以 \'/\' 开头,可以包含数字,字母和以下符号:_-/',
+ 'url_not_unique' => 'URL 已存在',
+ 'layout' => '布局',
+ 'layouts_not_found' => '找不到布局文件',
+ 'saved' => '保存成功!',
+ 'tab' => '页面',
+ 'manage_pages' => '管理静态页面',
+ 'manage_menus' => '管理静态菜单',
+ 'access_snippets' => '代码片段',
+ 'manage_content' => '管理静态内容',
+ ],
+ 'menu' => [
+ 'menu_label' => '菜单',
+ 'delete_confirmation' => '你真的要删除所选菜单吗?',
+ 'no_records' => '找不到菜单',
+ 'new' => '新建',
+ 'new_name' => '未命名的菜单',
+ 'new_code' => 'new-menu',
+ 'delete_confirm_single' => '你真的要删除这个菜单吗?',
+ 'saved' => '保存成功!',
+ 'name' => '名称',
+ 'code' => '编码',
+ 'items' => '菜单项',
+ 'add_subitem' => '插入子项',
+ 'code_required' => '编码是必要的!',
+ 'invalid_code' => '编码格式无效,该编码可以包含数字,字母和以下符号:_-',
+ ],
+ 'menuitem' => [
+ 'title' => '标题',
+ 'editor_title' => '编辑菜单项',
+ 'type' => '类型',
+ 'allow_nested_items' => '允许嵌套',
+ 'allow_nested_items_comment' => '嵌套项目可以通过静态页面和其他一些项目类型动态生成',
+ 'url' => 'URL',
+ 'reference' => '参考',
+ 'search_placeholder' => '搜索所有参考...',
+ 'title_required' => '标题是必需的',
+ 'unknown_type' => '未知的菜单项类型',
+ 'unnamed' => '未命名的菜单项',
+ 'add_item' => '掺入菜单项I ',
+ 'new_item' => '新菜单项',
+ 'replace' => '将此项目替换为生成的子项',
+ 'replace_comment' => '使用此复选框产生的菜单项推到与本项目同一层级。 该项目本身将被隐藏。',
+ 'cms_page' => 'CMS 页面',
+ 'cms_page_comment' => '当单击菜单项时,选择要打开的页面。',
+ 'reference_required' => '菜单项参考 是必需的。',
+ 'url_required' => 'URL 是必需的。',
+ 'cms_page_required' => '请选择 CMS 页面',
+ 'code' => '编码',
+ 'code_comment' => '如果要使用 API 访问菜单项代码,请输入。',
+ 'static_page' => '静态页面',
+ 'all_static_pages' => '所有静态页'
+ ],
+ 'content' => [
+ 'menu_label' => '内容',
+ 'cant_save_to_dir' => '将内容文件保存到 \'static-pages\' 目录是不允许的。',
+ ],
+ 'sidebar' => [
+ 'add' => '插入',
+ 'search' => '检索...',
+ ],
+ 'object' => [
+ 'invalid_type' => '未知的对象类型',
+ 'not_found' => '找不到请求的对象。',
+ ],
+ 'editor' => [
+ 'title' => '标题',
+ 'new_title' => '未命名',
+ 'content' => '内容',
+ 'url' => 'URL',
+ 'filename' => '文件名',
+ 'layout' => '布局',
+ 'description' => '描述',
+ 'preview' => '预览',
+ 'enter_fullscreen' => '全屏编辑',
+ 'exit_fullscreen' => '退出全屏',
+ 'hidden' => '隐藏',
+ 'hidden_comment' => '隐藏的页面只能由登录的后台用户访问。',
+ 'navigation_hidden' => '在导航中隐藏',
+ 'navigation_hidden_comment' => '选中此框可将该页面在自动生成的菜单和面包屑导航中隐藏起来。',
+ ],
+ 'snippet' => [
+ 'partialtab' => 'Snippet',
+ 'code' => 'Snippet code',
+ 'code_comment' => '输入一个代码,使其部分可用作“静态页面”插件中的代码段。',
+ 'name' => '名称',
+ 'name_comment' => '该名称显示在“静态页面”侧边栏的代码段列表中,并在添加代码段时显示在该页面上。',
+ 'no_records' => '找不到代码片段',
+ 'menu_label' => '代码片段',
+ 'column_property' => '性质',
+ 'column_type' => '类型',
+ 'column_code' => '编码',
+ 'column_default' => '默认',
+ 'column_options' => '选项',
+ 'column_type_string' => '字符串',
+ 'column_type_checkbox' => '检查框',
+ 'column_type_dropdown' => '下拉框',
+ 'not_found' => '代码段与请求的 code:code 没有在主题中找到。',
+ 'property_format_error' => '属性代码应以字母开头,只能包含字母和数字',
+ 'invalid_option_key' => '无效的下拉选项 key: :key。 选项键只能包含数字,字母和字符 _ 和 -',
+ ],
+ 'component' => [
+ 'static_page_name' => '静态页',
+ 'static_page_description' => '在 CMS 布局中输出静态页。',
+ 'static_page_use_content_name' => '使用页面内容字段',
+ 'static_page_use_content_description' => '如果未选中,编辑静态页面时内容部分将不会出现。 页面内容将仅通过占位符和变量来确定。',
+ 'static_page_default_name' => '默认布局',
+ 'static_page_default_description' => '将此布局定义为新页面的默认设置',
+ 'static_page_child_layout_name' => '子页面布局',
+ 'static_page_child_layout_description' => '该布局用作任何新子页面的默认布局',
+ 'static_menu_name' => '静态菜单',
+ 'static_menu_description' => '输出 CMS 布局中的菜单。',
+ 'static_menu_code_name' => '菜单',
+ 'static_menu_code_description' => '指定组件应输出的菜单代码。',
+ 'static_breadcrumbs_name' => '静态面包屑导航',
+ 'static_breadcrumbs_description' => '输出静态页面的面包屑导航。',
+ ]
+];
diff --git a/plugins/rainlab/pages/updates/snippets_rename_viewbag_properties.php b/plugins/rainlab/pages/updates/snippets_rename_viewbag_properties.php
new file mode 100644
index 000000000..cdca3871b
--- /dev/null
+++ b/plugins/rainlab/pages/updates/snippets_rename_viewbag_properties.php
@@ -0,0 +1,34 @@
+all();
+ foreach ($partials as $partial) {
+ try {
+ $path = $partial->getFilePath();
+ $contents = File::get($path);
+ if (strpos($contents, 'staticPageSnippetCode') === false) continue;
+ $contents = str_replace('staticPageSnippetName', 'snippetName', $contents);
+ $contents = str_replace('staticPageSnippetCode', 'snippetCode', $contents);
+ $contents = str_replace('staticPageSnippetProperties', 'snippetProperties', $contents);
+ File::put($path, $contents);
+ }
+ catch (\Exception $ex) { continue; }
+ }
+ }
+ }
+
+ public function down()
+ {
+ }
+}
diff --git a/plugins/rainlab/pages/updates/version.yaml b/plugins/rainlab/pages/updates/version.yaml
new file mode 100644
index 000000000..1cc521005
--- /dev/null
+++ b/plugins/rainlab/pages/updates/version.yaml
@@ -0,0 +1,60 @@
+1.0.1: Implemented the static pages management and the Static Page component.
+1.0.2: Fixed the page preview URL.
+1.0.3: Implemented menus.
+1.0.4: Implemented the content block management and placeholder support.
+1.0.5: Added support for the Sitemap plugin.
+1.0.6: Minor updates to the internal API.
+1.0.7: Added the Snippets feature.
+1.0.8: Minor improvements to the code.
+1.0.9: Fixes issue where Snippet tab is missing from the Partials form.
+1.0.10: Add translations for various locales.
+1.0.11: Fixes issue where placeholders tabs were missing from Page form.
+1.0.12: Implement Media Manager support.
+1.1.0:
+ - Adds meta title and description to pages. Adds |staticPage filter.
+ - snippets_rename_viewbag_properties.php
+1.1.1: Add support for Syntax Fields.
+1.1.2: Static Breadcrumbs component now respects the hide from navigation setting.
+1.1.3: Minor back-end styling fix.
+1.1.4: Minor fix to the StaticPage component API.
+1.1.5: Fixes bug when using syntax fields.
+1.1.6: Minor styling fix to the back-end UI.
+1.1.7: Improved menu item form to include CSS class, open in a new window and hidden flag.
+1.1.8: Improved the output of snippet partials when saved.
+1.1.9: Minor update to snippet inspector internal API.
+1.1.10: Fixes a bug where selecting a layout causes permanent unsaved changes.
+1.1.11: Add support for repeater syntax field.
+1.2.0: Added support for translations, UI updates.
+1.2.1: Use nice titles when listing the content files.
+1.2.2: Minor styling update.
+1.2.3: Snippets can now be moved by dragging them.
+1.2.4: Fixes a bug where the cursor is misplaced when editing text files.
+1.2.5: Fixes a bug where the parent page is lost upon changing a page layout.
+1.2.6: Shared view variables are now passed to static pages.
+1.2.7: Fixes issue with duplicating properties when adding multiple snippets on the same page.
+1.2.8: Fixes a bug where creating a content block without extension doesn't save the contents to file.
+1.2.9: Add conditional support for translating page URLs.
+1.2.10: Streamline generation of URLs to use the new Cms::url helper.
+1.2.11: Implements repeater usage with translate plugin.
+1.2.12: Fixes minor issue when using snippets and switching the application locale.
+1.2.13: Fixes bug when AJAX is used on a page that does not yet exist.
+1.2.14: Add theme logging support for changes made to menus.
+1.2.15: Back-end navigation sort order updated.
+1.2.16: Fixes a bug when saving a template that has been modified outside of the CMS (mtime mismatch).
+1.2.17: Changes locations of custom fields to secondary tabs instead of the primary Settings area. New menu search ability on adding menu items
+1.2.18: Fixes cache-invalidation issues when RainLab.Translate is not installed. Added Greek & Simplified Chinese translations. Removed deprecated calls. Allowed saving HTML in snippet properties. Added support for the MediaFinder in menu items.
+1.2.19: Catch exception with corrupted menu file.
+1.2.20: StaticMenu component now exposes menuName property; added pages.menu.referencesGenerated event.
+1.2.21: Fixes a bug where last Static Menu item cannot be deleted. Improved Persian, Slovak and Turkish translations.
+1.3.0: Added support for using Database-driven Themes when enabled in the CMS configuration.
+1.3.1: Added ChildPages Component, prevent hidden pages from being returned via menu item resolver.
+1.3.2: Fixes error when creating a subpage whose parent has no layout set.
+1.3.3: Improves user experience for users with only partial access through permissions
+1.3.4: Fix error where large menus were being truncated due to the PHP "max_input_vars" configuration value. Improved Slovenian translation.
+1.3.5: Minor fix to bust the browser cache for JS assets. Prevent duplicate property fields in snippet inspector.
+1.3.6: ChildPages component now displays localized page titles from Translate plugin.
+1.3.7: Adds MenuPicker formwidget. Adds future support for v2.0 of October CMS.
+1.4.0: Fixes bug when adding menu items in October CMS v2.0.
+1.4.1: Fixes support for configuration values.
+1.4.3: Fixes page deletion is newer platform builds.
+1.4.4: Disable touch device detection
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/MenuList.php b/plugins/rainlab/pages/widgets/MenuList.php
new file mode 100644
index 000000000..ccef3efef
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/MenuList.php
@@ -0,0 +1,122 @@
+alias = $alias;
+ $this->theme = Theme::getEditTheme();
+ $this->dataIdPrefix = 'page-'.$this->theme->getDirName();
+
+ parent::__construct($controller, []);
+ $this->bindToController();
+ }
+
+ /**
+ * Renders the widget.
+ * @return string
+ */
+ public function render()
+ {
+ return $this->makePartial('body', [
+ 'data' => $this->getData()
+ ]);
+ }
+
+ //
+ // Event handlers
+ //
+
+ public function onUpdate()
+ {
+ $this->extendSelection();
+
+ return $this->updateList();
+ }
+
+ public function onSearch()
+ {
+ $this->setSearchTerm(Input::get('search'));
+ $this->extendSelection();
+
+ return $this->updateList();
+ }
+
+ //
+ // Methods for the internal use
+ //
+
+ protected function getData()
+ {
+ $menus = Menu::listInTheme($this->theme, true);
+
+ $searchTerm = Str::lower($this->getSearchTerm());
+
+ if (strlen($searchTerm)) {
+ $words = explode(' ', $searchTerm);
+ $filteredMenus = [];
+
+ foreach ($menus as $menu) {
+ if ($this->textMatchesSearch($words, $menu->name.' '.$menu->fileName)) {
+ $filteredMenus[] = $menu;
+ }
+ }
+
+ $menus = $filteredMenus;
+ }
+
+ return $menus;
+ }
+
+ protected function updateList()
+ {
+ $vars = ['items' => $this->getData()];
+ return ['#'.$this->getId('menu-list') => $this->makePartial('items', $vars)];
+ }
+
+ protected function getThemeSessionKey($prefix)
+ {
+ return $prefix . $this->theme->getDirName();
+ }
+
+ protected function getSession($key = null, $default = null)
+ {
+ $key = strlen($key) ? $this->getThemeSessionKey($key) : $key;
+
+ return parent::getSession($key, $default);
+ }
+
+ protected function putSession($key, $value)
+ {
+ return parent::putSession($this->getThemeSessionKey($key), $value);
+ }
+}
diff --git a/plugins/rainlab/pages/widgets/PageList.php b/plugins/rainlab/pages/widgets/PageList.php
new file mode 100644
index 000000000..956c6b1d9
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/PageList.php
@@ -0,0 +1,168 @@
+alias = $alias;
+ $this->theme = Theme::getEditTheme();
+ $this->dataIdPrefix = 'page-'.$this->theme->getDirName();
+
+ parent::__construct($controller, []);
+ $this->bindToController();
+ }
+
+ /**
+ * Renders the widget.
+ * @return string
+ */
+ public function render()
+ {
+ return $this->makePartial('body', [
+ 'data' => $this->getData()
+ ]);
+ }
+
+ /*
+ * Event handlers
+ */
+
+ public function onReorder()
+ {
+ $structure = json_decode(Input::get('structure'), true);
+ if (!$structure) {
+ throw new SystemException('Invalid structure data posted.');
+ }
+
+ $pageList = new StaticPageList($this->theme);
+ $pageList->updateStructure($structure);
+ }
+
+ public function onUpdate()
+ {
+ $this->extendSelection();
+
+ return $this->updateList();
+ }
+
+ public function onSearch()
+ {
+ $this->setSearchTerm(Input::get('search'));
+ $this->extendSelection();
+
+ return $this->updateList();
+ }
+
+ /*
+ * Methods for internal use
+ */
+
+ protected function getData()
+ {
+ $pageList = new StaticPageList($this->theme);
+ $pages = $pageList->getPageTree(true);
+
+ $searchTerm = Str::lower($this->getSearchTerm());
+
+ if (strlen($searchTerm)) {
+ $words = explode(' ', $searchTerm);
+
+ $iterator = function($pages) use (&$iterator, $words) {
+ $result = [];
+
+ foreach ($pages as $page) {
+ if ($this->textMatchesSearch($words, $this->subtreeToText($page))) {
+ $result[] = (object) [
+ 'page' => $page->page,
+ 'subpages' => $iterator($page->subpages)
+ ];
+ }
+ }
+
+ return $result;
+ };
+
+ $pages = $iterator($pages);
+ }
+
+ return $pages;
+ }
+
+ protected function getThemeSessionKey($prefix)
+ {
+ return $prefix.$this->theme->getDirName();
+ }
+
+ protected function updateList()
+ {
+ return ['#'.$this->getId('page-list') => $this->makePartial('items', ['items' => $this->getData()])];
+ }
+
+ protected function subtreeToText($page)
+ {
+ $result = $this->pageToText($page->page);
+
+ $iterator = function($pages) use (&$iterator, &$result) {
+ foreach ($pages as $page) {
+ $result .= ' '.$this->pageToText($page->page);
+ $iterator($page->subpages);
+ }
+ };
+
+ $iterator($page->subpages);
+
+ return $result;
+ }
+
+ protected function pageToText($page)
+ {
+ $viewBag = $page->getViewBag();
+
+ return $page->getViewBag()->property('title').' '.$page->getViewBag()->property('url');
+ }
+
+ protected function getSession($key = null, $default = null)
+ {
+ $key = strlen($key) ? $this->getThemeSessionKey($key) : $key;
+
+ return parent::getSession($key, $default);
+ }
+
+ protected function putSession($key, $value)
+ {
+ return parent::putSession($this->getThemeSessionKey($key), $value);
+ }
+}
diff --git a/plugins/rainlab/pages/widgets/SnippetList.php b/plugins/rainlab/pages/widgets/SnippetList.php
new file mode 100644
index 000000000..7c4b88941
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/SnippetList.php
@@ -0,0 +1,113 @@
+alias = $alias;
+ $this->theme = Theme::getEditTheme();
+ $this->dataIdPrefix = 'snippet-'.$this->theme->getDirName();
+
+ parent::__construct($controller, []);
+ $this->bindToController();
+ }
+
+ /**
+ * Renders the widget.
+ * @return string
+ */
+ public function render()
+ {
+ return $this->makePartial('body', [
+ 'data' => $this->getData()
+ ]);
+ }
+
+ /*
+ * Event handlers
+ */
+
+ public function onSearch()
+ {
+ $this->setSearchTerm(Input::get('search'));
+
+ return $this->updateList();
+ }
+
+ /*
+ * Methods for the internal use
+ */
+
+ protected function getData()
+ {
+ $manager = SnippetManager::instance();
+ $snippets = $manager->listSnippets($this->theme);
+
+ $searchTerm = Str::lower($this->getSearchTerm());
+
+ if (strlen($searchTerm)) {
+ $words = explode(' ', $searchTerm);
+ $filteredSnippets = [];
+
+ foreach ($snippets as $snippet) {
+ if ($this->textMatchesSearch($words, $snippet->getName().' '.$snippet->code.' '.$snippet->getDescription())) {
+ $filteredSnippets[] = $snippet;
+ }
+ }
+
+ $snippets = $filteredSnippets;
+ }
+
+ usort($snippets, function($a, $b) {
+ return strcmp($a->getName(), $b->getName());
+ });
+
+ return $snippets;
+ }
+
+ protected function updateList()
+ {
+ return ['#'.$this->getId('snippet-list') => $this->makePartial('items', ['items' => $this->getData()])];
+ }
+
+ protected function getThemeSessionKey($prefix)
+ {
+ return $prefix.$this->theme->getDirName();
+ }
+
+ protected function getSession($key = null, $default = null)
+ {
+ $key = strlen($key) ? $this->getThemeSessionKey($key) : $key;
+
+ return parent::getSession($key, $default);
+ }
+
+ protected function putSession($key, $value)
+ {
+ return parent::putSession($this->getThemeSessionKey($key), $value);
+ }
+}
diff --git a/plugins/rainlab/pages/widgets/menulist/partials/_body.htm b/plugins/rainlab/pages/widgets/menulist/partials/_body.htm
new file mode 100644
index 000000000..da0e69633
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/menulist/partials/_body.htm
@@ -0,0 +1,10 @@
+
+= $this->makePartial('toolbar') ?>
+
+
+
+
+ = $this->makePartial('menus', ['data' => $data]) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/menulist/partials/_items.htm b/plugins/rainlab/pages/widgets/menulist/partials/_items.htm
new file mode 100644
index 000000000..a17162378
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/menulist/partials/_items.htm
@@ -0,0 +1,39 @@
+
+
+
+ = e(trans($this->noRecordsMessage)) ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/menulist/partials/_menus.htm b/plugins/rainlab/pages/widgets/menulist/partials/_menus.htm
new file mode 100644
index 000000000..c5f03acdf
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/menulist/partials/_menus.htm
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/menulist/partials/_toolbar.htm b/plugins/rainlab/pages/widgets/menulist/partials/_toolbar.htm
new file mode 100644
index 000000000..8c29f202c
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/menulist/partials/_toolbar.htm
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/pagelist/partials/_body.htm b/plugins/rainlab/pages/widgets/pagelist/partials/_body.htm
new file mode 100644
index 000000000..cf5e883fa
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/pagelist/partials/_body.htm
@@ -0,0 +1,10 @@
+
+= $this->makePartial('toolbar') ?>
+
+
+
+
+ = $this->makePartial('pages', ['data' => $data]) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/pagelist/partials/_items.htm b/plugins/rainlab/pages/widgets/pagelist/partials/_items.htm
new file mode 100644
index 000000000..61b079193
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/pagelist/partials/_items.htm
@@ -0,0 +1,9 @@
+
+
+ = $this->makePartial('treebranch', ['items' => $items]) ?>
+
+
+ = trans($this->noRecordsMessage) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/pagelist/partials/_pages.htm b/plugins/rainlab/pages/widgets/pagelist/partials/_pages.htm
new file mode 100644
index 000000000..4748886cc
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/pagelist/partials/_pages.htm
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/pagelist/partials/_toolbar.htm b/plugins/rainlab/pages/widgets/pagelist/partials/_toolbar.htm
new file mode 100644
index 000000000..21a315131
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/pagelist/partials/_toolbar.htm
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/pagelist/partials/_treebranch.htm b/plugins/rainlab/pages/widgets/pagelist/partials/_treebranch.htm
new file mode 100644
index 000000000..acf475241
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/pagelist/partials/_treebranch.htm
@@ -0,0 +1,55 @@
+
+ page->getBaseFileName();
+ $groupStatus = $this->getCollapseStatus($fileName);
+ $dataId = $this->dataIdPrefix.'-'.$fileName;
+ $searchMode = strlen($this->getSearchTerm()) > 0;
+ $cbId = 'cb'.md5($fileName);
+ ?>
+ data-no-drag-mode
+ data-id="= e($dataId) ?>"
+ >
+
+
+
+ subpages): ?>
+ = $this->makePartial('treebranch', ['items' => $subpages]) ?>
+
+
+
+
diff --git a/plugins/rainlab/pages/widgets/snippetlist/partials/_body.htm b/plugins/rainlab/pages/widgets/snippetlist/partials/_body.htm
new file mode 100644
index 000000000..807c29327
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/snippetlist/partials/_body.htm
@@ -0,0 +1,8 @@
+= $this->makePartial('toolbar') ?>
+
+
+
+ = $this->makePartial('snippets', ['data'=>$data]) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/snippetlist/partials/_items.htm b/plugins/rainlab/pages/widgets/snippetlist/partials/_items.htm
new file mode 100644
index 000000000..b0c85ee26
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/snippetlist/partials/_items.htm
@@ -0,0 +1,28 @@
+
+
+
+ = e(trans($this->noRecordsMessage)) ?>
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/snippetlist/partials/_snippets.htm b/plugins/rainlab/pages/widgets/snippetlist/partials/_snippets.htm
new file mode 100644
index 000000000..bd8989c67
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/snippetlist/partials/_snippets.htm
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/pages/widgets/snippetlist/partials/_toolbar.htm b/plugins/rainlab/pages/widgets/snippetlist/partials/_toolbar.htm
new file mode 100644
index 000000000..84094f482
--- /dev/null
+++ b/plugins/rainlab/pages/widgets/snippetlist/partials/_toolbar.htm
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/shablon/index.html b/shablon/index.html
index 10db1de72..b258d21b0 100644
--- a/shablon/index.html
+++ b/shablon/index.html
@@ -874,4 +874,4 @@