mirror of
https://github.com/typemill/typemill.git
synced 2025-01-17 21:38:25 +01:00
Version 1.3.7: Image Field for Themes and Plugins
This commit is contained in:
parent
b5b4a11baf
commit
b41ca0a3a1
@ -1,4 +1,4 @@
|
||||
# Typemill
|
||||
|
||||
*Typemill is a user-friendly and lightweight open source CMS for publishing text-works like prose, lyrics, manuals, documentations, studies and more. Just download and start.*
|
||||
Typemill is a user-friendly and lightweight open source CMS for publishing text-works like prose, lyrics, manuals, documentations, studies and more. Just download and start.
|
||||
|
||||
|
@ -172,7 +172,7 @@ class SettingsController extends Controller
|
||||
|
||||
public function showThemes($request, $response, $args)
|
||||
{
|
||||
$userSettings = $this->c->get('settings');
|
||||
$userSettings = $this->c->get('settings');
|
||||
$themes = $this->getThemes();
|
||||
$themedata = array();
|
||||
$fieldsModel = new Fields();
|
||||
@ -332,7 +332,7 @@ class SettingsController extends Controller
|
||||
$userInput = isset($params[$themeName]) ? $params[$themeName] : false;
|
||||
$validate = new Validation();
|
||||
$themeSettings = \Typemill\Settings::getObjectSettings('themes', $themeName);
|
||||
|
||||
|
||||
if(isset($themeSettings['settings']['images']))
|
||||
{
|
||||
# get the default settings
|
||||
@ -375,12 +375,23 @@ class SettingsController extends Controller
|
||||
|
||||
if($userInput)
|
||||
{
|
||||
/* validate the user-input */
|
||||
$this->validateInput('themes', $themeName, $userInput, $validate);
|
||||
|
||||
# validate the user-input and return image-fields if they are defined
|
||||
$imageFields = $this->validateInput('themes', $themeName, $userInput, $validate);
|
||||
|
||||
/* set user input as theme settings */
|
||||
$userSettings['themes'][$themeName] = $userInput;
|
||||
}
|
||||
|
||||
# handle images
|
||||
$images = $request->getUploadedFiles();
|
||||
|
||||
if(!isset($_SESSION['errors']) && isset($images[$themeName]))
|
||||
{
|
||||
$userInput = $this->saveImages($imageFields, $userInput, $userSettings, $images[$themeName]);
|
||||
|
||||
# set user input as theme settings
|
||||
$userSettings['themes'][$themeName] = $userInput;
|
||||
}
|
||||
|
||||
/* check for errors and redirect to path, if errors found */
|
||||
if(isset($_SESSION['errors']))
|
||||
@ -428,11 +439,22 @@ class SettingsController extends Controller
|
||||
else
|
||||
{
|
||||
/* validate the user-input */
|
||||
$this->validateInput('plugins', $pluginName, $userInput[$pluginName], $validate);
|
||||
$imageFields = $this->validateInput('plugins', $pluginName, $userInput[$pluginName], $validate);
|
||||
|
||||
/* use the input data */
|
||||
$pluginSettings[$pluginName] = $userInput[$pluginName];
|
||||
}
|
||||
|
||||
# handle images
|
||||
$images = $request->getUploadedFiles();
|
||||
|
||||
if(!isset($_SESSION['errors']) && isset($images[$pluginName]))
|
||||
{
|
||||
$userInput[$pluginName] = $this->saveImages($imageFields, $userInput[$pluginName], $userSettings, $images[$pluginName]);
|
||||
|
||||
# set user input as theme settings
|
||||
$pluginSettings[$pluginName] = $userInput[$pluginName];
|
||||
}
|
||||
|
||||
/* deactivate the plugin, if there is no active flag */
|
||||
if(!isset($userInput[$pluginName]['active']))
|
||||
@ -465,6 +487,9 @@ class SettingsController extends Controller
|
||||
/* fetch the original settings from the folder (plugin or theme) to get the field definitions */
|
||||
$originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName);
|
||||
|
||||
# images get special treatment
|
||||
$imageFieldDefinitions = array();
|
||||
|
||||
if(isset($originalSettings['forms']['fields']))
|
||||
{
|
||||
/* flaten the multi-dimensional array with fieldsets to a one-dimensional array */
|
||||
@ -509,6 +534,12 @@ class SettingsController extends Controller
|
||||
{
|
||||
/* validate user input for this field */
|
||||
$validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition, $skiprequired);
|
||||
|
||||
if($fieldDefinition['type'] == 'image')
|
||||
{
|
||||
# we want to return all images-fields for further processing
|
||||
$imageFieldDefinitions[$fieldName] = $fieldDefinition;
|
||||
}
|
||||
}
|
||||
if(!$fieldDefinition && $fieldName != 'active')
|
||||
{
|
||||
@ -516,6 +547,45 @@ class SettingsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $imageFieldDefinitions;
|
||||
}
|
||||
|
||||
protected function saveImages($imageFields, $userInput, $userSettings, $files)
|
||||
{
|
||||
|
||||
# initiate image processor with standard image sizes
|
||||
$processImages = new ProcessImage($userSettings['images']);
|
||||
|
||||
if(!$processImages->checkFolders())
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.');
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach($imageFields as $fieldName => $imageField)
|
||||
{
|
||||
if(isset($userInput[$fieldName]))
|
||||
{
|
||||
# handle single input with single file upload
|
||||
$image = $files[$fieldName];
|
||||
|
||||
if($image->getError() === UPLOAD_ERR_OK)
|
||||
{
|
||||
# not the most elegant, but createImage expects a base64-encoded string.
|
||||
$imageContent = $image->getStream()->getContents();
|
||||
$imageData = base64_encode($imageContent);
|
||||
$imageSrc = 'data: ' . $image->getClientMediaType() . ';base64,' . $imageData;
|
||||
|
||||
if($processImages->createImage($imageSrc, $image->getClientFilename(), $userSettings['images'], $overwrite = NULL))
|
||||
{
|
||||
# returns image path to media library
|
||||
$userInput[$fieldName] = $processImages->publishImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $userInput;
|
||||
}
|
||||
|
||||
/***********************
|
||||
@ -751,13 +821,13 @@ class SettingsController extends Controller
|
||||
|
||||
private function getLanguages()
|
||||
{
|
||||
return array(
|
||||
'en' => 'English',
|
||||
'ru' => 'Russian',
|
||||
'nl' => 'Dutch, Flemish',
|
||||
'de' => 'German',
|
||||
'it' => 'Italian',
|
||||
'fr' => 'French',
|
||||
);
|
||||
}
|
||||
}
|
||||
return array(
|
||||
'en' => 'English',
|
||||
'ru' => 'Russian',
|
||||
'nl' => 'Dutch, Flemish',
|
||||
'de' => 'German',
|
||||
'it' => 'Italian',
|
||||
'fr' => 'French',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ class Fields
|
||||
}
|
||||
else
|
||||
{
|
||||
$field->unsetAttribute('chhecked');
|
||||
$field->unsetAttribute('checked');
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -506,7 +506,9 @@ class Folder
|
||||
{
|
||||
# if it is the first round, create an empty array
|
||||
if(!$i){ $i = 0; $breadcrumb = array();}
|
||||
|
||||
|
||||
if(!$searchArray){ return $breadcrumb;}
|
||||
|
||||
while($i < count($searchArray))
|
||||
{
|
||||
if(!isset($content[$searchArray[$i]])){ return false; }
|
||||
|
@ -27,6 +27,15 @@ class Validation
|
||||
return false;
|
||||
}, 'invalid values');
|
||||
|
||||
Validator::addRule('image_types', function($field, $value, array $params, array $fields) use ($user)
|
||||
{
|
||||
$allowed = ['jpg', 'jpeg', 'png'];
|
||||
$pathinfo = pathinfo($value);
|
||||
$extension = strtolower($pathinfo['extension']);
|
||||
if(array_search($extension, $allowed)){ return true; }
|
||||
return false;
|
||||
}, 'only jpg, jpeg, png allowed');
|
||||
|
||||
Validator::addRule('userAvailable', function($field, $value, array $params, array $fields) use ($user)
|
||||
{
|
||||
$userdata = $user->getUser($value);
|
||||
@ -434,6 +443,7 @@ class Validation
|
||||
case "image":
|
||||
$v->rule('noHTML', $fieldName);
|
||||
$v->rule('lengthMax', $fieldName, 1000);
|
||||
$v->rule('image_types', $fieldName);
|
||||
break;
|
||||
default:
|
||||
$v->rule('lengthMax', $fieldName, 1000);
|
||||
|
@ -1114,7 +1114,7 @@ ul.cardInfo{
|
||||
border: 0px;
|
||||
}
|
||||
.cardFields.open{
|
||||
max-height: 5000px;
|
||||
max-height: 20000px;
|
||||
transition: max-height 0.5s ease-in;
|
||||
overflow: hidden;
|
||||
border: 1px;
|
||||
@ -2490,6 +2490,8 @@ footer a:focus, footer a:hover, footer a:active
|
||||
}
|
||||
.blox .TOC li:before{ color: #bbb; }
|
||||
|
||||
.mbfix{ margin-bottom: 0px!important; }
|
||||
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
section{
|
||||
|
@ -1,3 +1,5 @@
|
||||
{% set itemurl = (item.urlRelWoF == '/') ? '' : item.urlRelWoF %}
|
||||
|
||||
<div class="editor buttonset" id="publishController" data-published="{{ item.published }}" data-drafted="{{ item.drafted }}" v-cloak>
|
||||
<div v-if="errors.message" class="message error">${ errors.message }</div>
|
||||
<button v-if="raw" @click.prevent="saveDraft" id="draft" :class="draftResult" :disabled="draftDisabled"><span class="desktop">{{ __('Save') }} </span>{{ __('Draft') }}</button><button @click.prevent="publishDraft" id="publish" :class="publishResult" :disabled="publishDisabled">{{ __('Publish') }}</button>
|
||||
@ -5,8 +7,8 @@
|
||||
<div class="secondary">
|
||||
<button @click.prevent="depublishArticle" class="button--secondary" :disabled="publishStatus"><span class="desktop">${publishLabel}</span><span class="mobile">${publishLabelMobile}</span></button>
|
||||
<button @click.prevent="showModal('delete')" class="button--secondary danger"><span class="desktop">{{ __('delete') }}</span><span class="mobile">X</span></button>
|
||||
<a v-if="visual" href="{{ base_url }}/tm/content/raw{{item.urlRelWoF}}" class="button--secondary"><span class="desktop">{{ __('raw mode') }}</span><span class="mobile">{{ __('raw') }}</span></a>
|
||||
<a v-if="raw" href="{{ base_url }}/tm/content/visual{{item.urlRelWoF}}" class="button--secondary"><span class="desktop">{{ __('visual mode') }}</span><span class="mobile">{{ __('visual') }}</span></a>
|
||||
<a v-if="visual" href="{{ base_url }}/tm/content/raw{{ itemurl }}" class="button--secondary"><span class="desktop">{{ __('raw mode') }}</span><span class="mobile">{{ __('raw') }}</span></a>
|
||||
<a v-if="raw" href="{{ base_url }}/tm/content/visual{{ itemurl }}" class="button--secondary"><span class="desktop">{{ __('visual mode') }}</span><span class="mobile">{{ __('visual') }}</span></a>
|
||||
<a target="_blank" class="button--secondary" href="{{ item.urlAbs }}"><svg class="icon baseline icon-external-link"><use xlink:href="#icon-external-link"></use></svg></a>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
|
@ -5,6 +5,7 @@ let typemillUtilities = {
|
||||
{
|
||||
this.youtubeItems = document.querySelectorAll( ".youtube" );
|
||||
},
|
||||
|
||||
addYoutubePlayButtons: function(){
|
||||
if(this.youtubeItems)
|
||||
{
|
||||
@ -24,12 +25,13 @@ let typemillUtilities = {
|
||||
youtubePlaybutton.classList.add("play-video");
|
||||
youtubePlaybutton.value = "Play";
|
||||
|
||||
element.parentNode.appendChild(youtubePlaybutton);
|
||||
element.parentNode.appendChild(youtubePlaybutton);
|
||||
},
|
||||
|
||||
listenToYoutube: function(){
|
||||
listenToClick: function(){
|
||||
document.addEventListener('click', function (event) {
|
||||
|
||||
/* listen to youtube */
|
||||
if (event.target.matches('.play-video')) {
|
||||
|
||||
var youtubeID = event.target.parentNode.getElementsByClassName('youtube')[0].id;
|
||||
@ -49,12 +51,66 @@ let typemillUtilities = {
|
||||
videocontainer.innerHTML = "";
|
||||
videocontainer.appendChild( iframe );
|
||||
}
|
||||
|
||||
if (event.target.matches('.function-delete-img')) {
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var imgUploadField = event.target.closest(".img-upload");
|
||||
var imgSrc = imgUploadField.getElementsByClassName("function-img-src")[0];
|
||||
imgSrc.src = '';
|
||||
var imgUrl = imgUploadField.getElementsByClassName("function-img-url")[0];
|
||||
imgUrl.value = '';
|
||||
|
||||
}
|
||||
|
||||
}, true);
|
||||
},
|
||||
|
||||
listenToChange: function()
|
||||
{
|
||||
document.addEventListener('change', function (changeevent) {
|
||||
|
||||
/* listen to youtube */
|
||||
if (changeevent.target.matches('.function-img-file')) {
|
||||
|
||||
if(changeevent.target.files.length > 0)
|
||||
{
|
||||
let imageFile = changeevent.target.files[0];
|
||||
let size = imageFile.size / 1024 / 1024;
|
||||
|
||||
if (!imageFile.type.match('image.*'))
|
||||
{
|
||||
// publishController.errors.message = "Only images are allowed.";
|
||||
}
|
||||
else if (size > this.maxsize)
|
||||
{
|
||||
// publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB";
|
||||
}
|
||||
else
|
||||
{
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(imageFile);
|
||||
reader.onload = function(fileevent)
|
||||
{
|
||||
var imgUploadField = changeevent.target.closest(".img-upload");
|
||||
var imgSrc = imgUploadField.getElementsByClassName("function-img-src")[0];
|
||||
imgSrc.src = fileevent.target.result;
|
||||
var imgUrl = imgUploadField.getElementsByClassName("function-img-url")[0];
|
||||
imgUrl.value = imageFile.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, true);
|
||||
},
|
||||
|
||||
start: function(){
|
||||
this.setYoutubeItems();
|
||||
this.addYoutubePlayButtons();
|
||||
this.listenToYoutube();
|
||||
},
|
||||
this.addYoutubePlayButtons();
|
||||
this.listenToClick();
|
||||
this.listenToChange();
|
||||
},
|
||||
};
|
@ -167,6 +167,7 @@ ULIST: ulist
|
||||
UNKNOWN: Unbekannt
|
||||
UPDATE_USER: Nutzer aktualisieren
|
||||
UPLOAD_FILE: Datei hochladen
|
||||
UPLOAD_AN_IMAGE: Bild hochladen
|
||||
UPLOAD: hochladen
|
||||
USE_2_TO_20_CHARACTERS: 2 bis 20 Anschläge erlaubt.
|
||||
USE_2_TO_40_CHARACTERS: 2 to 40 Anschläge erlaubt.
|
||||
|
@ -56,7 +56,11 @@
|
||||
</article>
|
||||
<footer></footer>
|
||||
</div>
|
||||
<script src="{{ base_url }}/system/author/js/color-picker.min.js?20200505"></script>
|
||||
<!-- <script src="{{ base_url }}/system/author/js/color-picker.min.js?20200505"></script> -->
|
||||
<script src="{{ base_url }}/system/author/js/author.js?20200505"></script>
|
||||
<script src="{{ base_url }}/system/author/js/typemillutils.js?20200505"></script>
|
||||
<script>
|
||||
typemillUtilities.start()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -5,67 +5,99 @@
|
||||
{% if field.help %}<div class="help">?<span class="tooltip">{{__(field.help|slice(0,100))}}</span></div>{% endif %}
|
||||
</label>
|
||||
|
||||
{% if field.type == 'textarea' %}
|
||||
{% if field.type == 'image' %}
|
||||
<div class="img-upload flex flex-wrap item-start">
|
||||
<div class="w-50">
|
||||
<div class="w6 h6 bg-black-40 dtc v-mid bg-chess">
|
||||
<img src="{{ settings[object][itemName][field.name] }}" class="function-img-src mw6 max-h6 dt center">
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-50 ph3 lh-copy f6 relative">
|
||||
<div class="relative dib w-100">
|
||||
<input class="function-img-file absolute o-0 w-100 top-0 z-1 pointer" type="file" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" accept="image/*" />
|
||||
<div class="relative w-100 bn br1 bg-tm-green white pa3 ma0 tc"><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> {{ __('Upload an image') }}</div>
|
||||
</div>
|
||||
<div class="dib w-100 mt3">
|
||||
<label>Image URL</label>
|
||||
<div class="flex">
|
||||
<button class="function-delete-img w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>
|
||||
<input class="function-img-url w-90 mbfix" type="text" name="{{ itemName }}[{{ field.name }}]" value="{{ settings[object][itemName][field.name] }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{% if errors[itemName][field.name] %}
|
||||
<div class="error f6">{{ errors[itemName][field.name] | first }}</div>
|
||||
{% endif %}
|
||||
|
||||
<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
|
||||
{% if field.description %}<div class="description pv3">{{ __(field.description) }}</div>{% endif %}
|
||||
|
||||
{% elseif field.type == 'paragraph' %}
|
||||
|
||||
{{ markdown(field.getContent()) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elseif field.type == 'checkbox' %}
|
||||
{% else %}
|
||||
|
||||
{% if field.type == 'textarea' %}
|
||||
|
||||
<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
|
||||
|
||||
{% elseif field.type == 'paragraph' %}
|
||||
|
||||
<label class="control-group">{{ __( field.getCheckboxLabel() ) }}
|
||||
<input type="checkbox" id="{{ itemName}}[{{ field.name }}]" name="{{ itemName}}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
{{ markdown(field.getContent()) }}
|
||||
|
||||
{% elseif field.type == 'checkboxlist' %}
|
||||
|
||||
{% set options = field.getOptions() %}
|
||||
|
||||
{% for value,label in options %}
|
||||
|
||||
<label class="control-group">{{ __( label ) }}
|
||||
<input type="checkbox" id="{{ itemName }}[{{ field.name }}][{{value}}]" name="{{ itemName }}[{{ field.name }}][{{value}}]" {{ settings[object][itemName][field.name][value] ? ' checked' : '' }}>
|
||||
{% elseif field.type == 'checkbox' %}
|
||||
|
||||
<label class="control-group">{{ __( field.getCheckboxLabel() ) }}
|
||||
<input type="checkbox" id="{{ itemName}}[{{ field.name }}]" name="{{ itemName}}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
|
||||
{% endfor %}
|
||||
{% elseif field.type == 'checkboxlist' %}
|
||||
|
||||
{% elseif field.type == 'select' %}
|
||||
{% set options = field.getOptions() %}
|
||||
|
||||
{% set options = field.getOptions() %}
|
||||
|
||||
<select id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
|
||||
{% for value,label in options %}
|
||||
<option value="{{ value }}" {{ (value == field.getAttributeValue('value')) ? ' selected' : '' }}>{{ label }}</option>
|
||||
|
||||
<label class="control-group">{{ __( label ) }}
|
||||
<input type="checkbox" id="{{ itemName }}[{{ field.name }}][{{value}}]" name="{{ itemName }}[{{ field.name }}][{{value}}]" {{ settings[object][itemName][field.name][value] ? ' checked' : '' }}>
|
||||
<span class="checkmark"></span>
|
||||
</label>
|
||||
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% elseif field.type == 'radio' %}
|
||||
{% elseif field.type == 'select' %}
|
||||
|
||||
{% set options = field.getOptions() %}
|
||||
{% set options = field.getOptions() %}
|
||||
|
||||
{% for value,label in options %}
|
||||
<select id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
|
||||
{% for value,label in options %}
|
||||
<option value="{{ value }}" {{ (value == field.getAttributeValue('value')) ? ' selected' : '' }}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% elseif field.type == 'radio' %}
|
||||
|
||||
{% set options = field.getOptions() %}
|
||||
|
||||
{% for value,label in options %}
|
||||
|
||||
<label class="control-group">{{ label }}
|
||||
<input type="radio" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" value="{{ value }}" {{ (value == settings[object][itemName][field.name]) ? ' checked' : '' }}>
|
||||
<span class="radiomark"></span>
|
||||
</label>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
<label class="control-group">{{ label }}
|
||||
<input type="radio" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" value="{{ value }}" {{ (value == settings[object][itemName][field.name]) ? ' checked' : '' }}>
|
||||
<span class="radiomark"></span>
|
||||
</label>
|
||||
{% else %}
|
||||
|
||||
{% endfor %}
|
||||
<input id="{{itemName}}[{{ field.name }}]" name="{{itemName}}[{{ field.name }}]" type="{{ field.type }}"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if field.description %}<div class="description">{{ __(field.description) }}</div>{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% if errors[itemName][field.name] %}
|
||||
<span class="error">{{ errors[itemName][field.name] | first }}</span>
|
||||
{% endif %}
|
||||
|
||||
<input id="{{itemName}}[{{ field.name }}]" name="{{itemName}}[{{ field.name }}]" type="{{ field.type }}"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if field.description %}<div class="description">{{ __(field.description) }}</div>{% endif %}
|
||||
|
||||
{% if errors[itemName][field.name] %}
|
||||
<span class="error">{{ errors[itemName][field.name] | first }}</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
@ -3,7 +3,7 @@
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ path_for('form.save') }}">
|
||||
<form method="POST" action="{{ path_for('form.save') }}" enctype="multipart/form-data">
|
||||
|
||||
<fieldset class="card{{ errors[itemName] ? ' errors' : '' }}">
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<div class="formWrapper">
|
||||
|
||||
<form method="POST" action="{{ path_for('plugins.save') }}" novalidate>
|
||||
<form method="POST" action="{{ path_for('plugins.save') }}" enctype="multipart/form-data" novalidate>
|
||||
|
||||
<section id="plugins" class="plugins">
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
{% for themeName, theme in themes %}
|
||||
|
||||
<form method="POST" id="theme-{{ themeName }}" action="{{ path_for('themes.save') }}">
|
||||
<form method="POST" id="theme-{{ themeName }}" action="{{ path_for('themes.save') }}" enctype="multipart/form-data">
|
||||
|
||||
<fieldset class="card{{ errors[themeName] ? ' errors' : '' }}">
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user