[WIP] Ajax Files Upload (#748)

* Reworked the `file` field. All files get uploaded via Ajax and are stored upon Save

This improves the Save task tremendously as now there is no longer the need of waiting for the files to finish uploading. Fully backward compatible, `file` field now includes also a `limit` and `filesize` option in the blueprints. The former determines how many files are allowed to be uploaded when in combination with `multiple: true` (default: 10), the latter determines the file size limit (in MB) allowed for each file (default: 5MB)

* Added support for `accept: [‘*’]` to allow any file type

* Minor tweaks in the comments and messages

* Delete any orphan file when discarding the uploaded files session

* Minor optimization

* Fixed issue with `_json` elements where nested fields merging would get stored in an unexpected way

* Potential fix for wrong order of value in Datetime

* Fixed nested fields for files

* Fixed tmp streams

* Minor cleanup

* Update JSON data when removing a file. Implemented task to remove files that haven’t been saved yet, from the flash object session

* Ensure temporary files are deleted when removing un-saved files from the flash object session

* Fixed wrong reference of HTML file field when clicking on the drop zone area to pick a file

* Added JSON template for pages

* fix a CSS issue in page order

* More CSS fixes

* Trigger file field mutation when adding or removing a file

* Recompiled JS

* Removed twig templates that are no longer needed

* Fixed issue with nested header fields in a page, not properly merging data

* [internal] Fixed issue with collections not capable of handling both param and dot notations at the same time

* Reorganized FileField structure to be more consistent with the other fields

* Added support for dynamically created file fields (ie, autoinitialization on new lists items)

* Added translationable strings for file uploads errors

* Added translasions for all Dropzone available strings

* Changed default values
This commit is contained in:
Djamil Legato
2016-08-29 11:12:09 -07:00
committed by GitHub
parent 2c07a1f209
commit 6b34336599
24 changed files with 807 additions and 483 deletions

View File

@@ -7,6 +7,8 @@
* More language strings added
* Added `clear-tmp` to cache clear dropdown
* Unified JSON twig templates
* Better error handling for 500 Internal Server Errors, when Fetch fails.
* Reworked the `file` field. All files get uploaded via Ajax and are stored upon Save. This improves the Save task tremendously as now there is no longer the need of waiting for the files to finish uploading. Fully backward compatible, `file` field now includes also a `limit` and `filesize` option in the blueprints. The former determines how many files are allowed to be uploaded when in combination with `multiple: true` (default: 10), the latter determines the file size limit (in MB) allowed for each file (default: 5MB)
1. [](#bugfix)
* Curl fix for invalid cert errors with News Feed
* Avoid requiring `admin.super` for ajax calls [#739](https://github.com/getgrav/grav-plugin-admin/issues/739)
@@ -14,6 +16,7 @@
* Fixed broken page type filtering
* Fixed `beforeunload` event not prompting to offer the choice to stay on the page in case of unsaved changes
* Fixed click-away detection for preventing loss of changes, that would get ignored in some circumstances (ie, from modal confirmation)
* Fixed issue with `_json` elements where nested fields merging would get stored in an unexpected way
# v1.1.4
## 08/14/2016

View File

@@ -364,6 +364,23 @@ class AdminPlugin extends Plugin
exit();
}
// Clear flash objects for previously uploaded files
// whenever the user switches page / reloads
// ignoring any JSON / extension call
if (is_null($this->uri->extension()) && $task !== 'save') {
// Discard any previously uploaded files session.
// and if there were any uploaded file, remove them from the filesystem
if ($flash = $this->session->getFlashObject('files-upload')) {
$flash = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($flash));
foreach ($flash as $key => $value) {
if ($key !== 'tmp_name') {
continue;
}
@unlink($value);
}
}
}
$self = $this;
// make sure page is not frozen!
@@ -583,6 +600,9 @@ class AdminPlugin extends Plugin
],
'list' => [
'array' => true
],
'file' => [
'array' => true
]
];
}
@@ -722,7 +742,17 @@ class AdminPlugin extends Plugin
'THEMES',
'ALL',
'FROM',
'TO'
'TO',
'DROPZONE_CANCEL_UPLOAD',
'DROPZONE_CANCEL_UPLOAD_CONFIRMATION',
'DROPZONE_DEFAULT_MESSAGE',
'DROPZONE_FALLBACK_MESSAGE',
'DROPZONE_FALLBACK_TEXT',
'DROPZONE_FILE_TOO_BIG',
'DROPZONE_INVALID_FILE_TYPE',
'DROPZONE_MAX_FILES_EXCEEDED',
'DROPZONE_REMOVE_FILE',
'DROPZONE_RESPONSE_ERROR'
];
foreach ($strings as $string) {

View File

@@ -107,6 +107,7 @@ class AdminController
$this->post = $this->getPost($post);
$this->route = $route;
$this->admin = $this->grav['admin'];
$this->uri = $this->grav['uri']->url();
}
/**
@@ -527,14 +528,12 @@ class AdminController
require_once __DIR__ . '/gpm.php';
$result = false;
try {
$result = \Grav\Plugin\Admin\Gpm::install($package, ['theme' => ($type == 'theme')]);
} catch (\Exception $e) {
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
return;
return false;
}
if ($result) {
@@ -588,18 +587,16 @@ class AdminController
$this->admin->json_response = ['status' => 'error', 'message' => $message];
return;
return false;
}
$result = false;
try {
$dependencies = $this->admin->dependenciesThatCanBeRemovedWhenRemoving($package);
$result = \Grav\Plugin\Admin\Gpm::uninstall($package, []);
} catch (\Exception $e) {
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
return;
return false;
}
if ($result) {
@@ -827,7 +824,7 @@ class AdminController
$this->admin->json_response = [
'status' => 'error'
];
return;
return false;
}
$filename = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT, true, true);
@@ -1249,9 +1246,9 @@ class AdminController
*/
protected function taskProcessMarkdown()
{
// if (!$this->authorizeTask('process markdown', ['admin.pages', 'admin.super'])) {
// return;
// }
/*if (!$this->authorizeTask('process markdown', ['admin.pages', 'admin.super'])) {
return;
}*/
try {
$page = $this->admin->page(true);
@@ -1442,169 +1439,6 @@ class AdminController
return true;
}
/**
* @param $field
*
* @return array
*/
private function cleanFilesData($field)
{
/** @var Page $page */
$page = null;
$cleanFiles = [];
$file = $_FILES['data'];
$errors = (array)Utils::getDotNotation($file['error'], $field['name']);
foreach ($errors as $index => $error) {
if ($error == UPLOAD_ERR_OK) {
$fieldname = $field['name'];
// Deal with multiple files
if (isset($field['multiple']) && $field['multiple'] == true) {
$fieldname = $fieldname . ".$index";
}
$tmp_name = Utils::getDotNotation($file['tmp_name'], $fieldname);
$name = Utils::getDotNotation($file['name'], $fieldname);
$type = Utils::getDotNotation($file['type'], $fieldname);
$size = Utils::getDotNotation($file['size'], $fieldname);
$original_destination = null;
$destination = Folder::getRelativePath(rtrim($field['destination'], '/'));
if (!$this->match_in_array($type, $field['accept'])) {
throw new \RuntimeException('File "' . $name . '" is not an accepted MIME type.');
}
if (isset($field['random_name']) && $field['random_name'] === true) {
$path_parts = pathinfo($name);
$name = Utils::generateRandomString(15) . '.' . $path_parts['extension'];
}
$resolved_destination = $this->admin->getPagePathFromToken($destination);
// Create dir if need be
if (!is_dir($resolved_destination)) {
Folder::mkdir($resolved_destination);
}
if (isset($field['avoid_overwriting']) && $field['avoid_overwriting'] === true) {
if (file_exists("$resolved_destination/$name")) {
$name = date('YmdHis') . '-' . $name;
}
}
if (move_uploaded_file($tmp_name, "$resolved_destination/$name")) {
$path = $destination . '/' . $name;
$fileData = [
'name' => $name,
'path' => $path,
'type' => $type,
'size' => $size,
'file' => $destination . '/' . $name,
'route' => $page ? $path : null
];
$cleanFiles[$field['name']][$path] = $fileData;
} else {
throw new \RuntimeException("Unable to upload file(s) to $destination/$name");
}
} else {
if ($error != UPLOAD_ERR_NO_FILE) {
throw new \RuntimeException("Unable to upload file(s) - Error: " . $field['name'] . ": " . $this->upload_errors[$error]);
}
}
}
return $cleanFiles;
}
/**
* @param string $needle
* @param array|string $haystack
*
* @return bool
*/
private function match_in_array($needle, $haystack)
{
foreach ((array)$haystack as $item) {
if (true == preg_match("#^" . strtr(preg_quote($item, '#'), ['\*' => '.*', '\?' => '.']) . "$#i",
$needle)
) {
return true;
}
}
return false;
}
/**
* @param mixed $obj
*
* @return mixed
*/
private function processFiles($obj)
{
if (!isset($_FILES['data'])) {
return $obj;
}
$blueprints = $obj->blueprints();
if (!isset($blueprints['form']['fields'])) {
throw new \RuntimeException('Blueprints missing form fields definition');
}
$fields = $blueprints['form']['fields'];
$found_files = $this->findFields('file', $fields);
foreach ($found_files as $key => $data) {
if ($this->view == 'pages') {
$keys = explode('.', preg_replace('/^header./', '', $key));
$init_key = array_shift($keys);
if (count($keys) > 0) {
$new_data = isset($obj->header()->$init_key) ? $obj->header()->$init_key : [];
Utils::setDotNotation($new_data, implode('.', $keys), $data);
} else {
$new_data = $data;
}
$obj->modifyHeader($init_key, $new_data);
} else {
$obj->set($key, $data);
}
}
return $obj;
}
public function findFields($type, $fields, $found = [])
{
foreach ($fields as $key => $field) {
if (isset($field['type']) && $field['type'] == $type) {
$file_field = $this->cleanFilesData($field);
} elseif (isset($field['fields'])) {
$result = $this->findFields($type, $field['fields'], $found);
if (!empty($result)) {
$found = array_merge($found, $result);
}
} else {
$file_field = null;
}
if (isset($file_field) && (!is_array($file_field) || !empty($file_field))) {
$found = array_merge($file_field, $found);
}
}
return $found;
}
/**
* Get the next available ordering number in a folder
*
@@ -1696,6 +1530,228 @@ class AdminController
return true;
}
/**
* Handles ajax upload for files.
* Stores in a flash object the temporary file and deals with potential file errors.
*
* @return bool True if the action was performed.
*/
public function taskFilesUpload()
{
if (!$this->authorizeTask('save', $this->dataPermissions()) || !isset($_FILES)) {
return false;
}
/** @var Config $config */
$config = $this->grav['config'];
$data = $this->view == 'pages' ? $this->admin->page(true) : $this->prepareData([]);
$settings = $data->blueprints()->schema()->getProperty($this->post['name']);
$settings = (object) array_merge(
['avoid_overwriting' => false,
'random_name' => false,
'accept' => ['image/*'],
'limit' => 10,
'filesize' => $config->get('system.media.upload_limit', 5242880) // 5MB
],
(array) $settings,
['name' => $this->post['name']]
);
$upload = $this->normalizeFiles($_FILES['data'], $settings->name);
// Do not use self@ outside of pages
if ($this->view != 'pages' && in_array($settings->destination, ['@self', 'self@'])) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null, true), $settings->destination)
];
return false;
}
// Handle errors and breaks without proceeding further
if ($upload->file->error != UPLOAD_ERR_OK) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $upload->file->name, $this->upload_errors[$upload->file->error])
];
return false;
} else {
// Remove the error object to avoid storing it
unset($upload->file->error);
// we need to move the file at this stage or else
// it won't be available upon save later on
// since php removes it from the upload location
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp_file = $upload->file->tmp_name;
$tmp = $tmp_dir . '/uploaded-files/' . basename($tmp_file);
Folder::create(dirname($tmp));
if (!move_uploaded_file($tmp_file, $tmp)) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '', $tmp)
];
return false;
}
$upload->file->tmp_name = $tmp;
}
// Handle file size limits
$settings->filesize *= 1048576; // 2^20 [MB in Bytes]
if ($settings->filesize > 0 && $upload->file->size > $settings->filesize) {
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
];
return false;
}
// Handle Accepted file types
// Accept can only be mime types (image/png | image/*) or file extensions (.pdf|.jpg)
$accepted = false;
$errors = [];
foreach ((array) $settings->accept as $type) {
// Force acceptance of any file when star notation
if ($type == '*') {
$accepted = true;
break;
}
$isMime = strstr($type, '/');
$find = str_replace('*', '.*', $type);
$match = preg_match('#'. $find .'$#', $isMime ? $upload->file->type : $upload->file->name);
if (!$match) {
$message = $isMime ? 'The MIME type "' . $upload->file->type . '"' : 'The File Extension';
$errors[] = $message . ' for the file "' . $upload->file->name . '" is not an accepted.';
$accepted |= false;
} else {
$accepted |= true;
}
}
if (!$accepted) {
$this->admin->json_response = [
'status' => 'error',
'message' => implode('<br />', $errors)
];
return false;
}
// Retrieve the current session of the uploaded files for the field
// and initialize it if it doesn't exist
$sessionField = base64_encode($this->uri);
$flash = $this->admin->session()->getFlashObject('files-upload');
if (!$flash) { $flash = []; }
if (!isset($flash[$sessionField])) { $flash[$sessionField] = []; }
if (!isset($flash[$sessionField][$upload->field])) { $flash[$sessionField][$upload->field] = []; }
// Set destination
$destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
$destination = $this->admin->getPagePathFromToken($destination);
// Create destination if needed
if (!is_dir($destination)) {
Folder::mkdir($destination);
}
// Generate random name if required
if ($settings->random_name) { // TODO: document
$extension = pathinfo($upload->file->name)['extension'];
$upload->file->name = Utils::generateRandomString(15) . '.' . $extension;
}
// Handle conflicting name if needed
if ($settings->avoid_overwriting) { // TODO: document
if (file_exists($destination . '/' . $upload->file->name)) {
$upload->file->name = date('YmdHis') . '-' . $upload->file->name;
}
}
// Prepare object for later save
$path = $destination . '/' . $upload->file->name;
$upload->file->path = $path;
// $upload->file->route = $page ? $path : null;
// Prepare data to be saved later
$flash[$sessionField][$upload->field][$path] = (array) $upload->file;
// Finally store the new uploaded file in the field session
$this->admin->session()->setFlashObject('files-upload', $flash);
$this->admin->json_response = [
'status' => 'success',
'session' => \json_encode([
'sessionField' => base64_encode($this->uri),
'path' => $upload->file->path,
'field' => $settings->name
])
];
return true;
}
/**
* Removes a file from the flash object session, before it gets saved
*
* @return bool True if the action was performed.
*/
public function taskFilesSessionRemove()
{
if (!$this->authorizeTask('save', $this->dataPermissions()) || !isset($_FILES)) {
return false;
}
// Retrieve the current session of the uploaded files for the field
// and initialize it if it doesn't exist
$sessionField = base64_encode($this->uri);
$request = \json_decode($this->post['session']);
// Ensure the URI requested matches the current one, otherwise fail
if ($request->sessionField !== $sessionField) {
return false;
}
// Retrieve the flash object and remove the requested file from it
$flash = $this->admin->session()->getFlashObject('files-upload');
$endpoint = $flash[$request->sessionField][$request->field][$request->path];
if (isset($endpoint)) {
if (file_exists($endpoint['tmp_name'])) {
unlink($endpoint['tmp_name']);
}
unset($endpoint);
}
// Walk backward to cleanup any empty field that's left
// Field
if (!count($flash[$request->sessionField][$request->field])) {
unset($flash[$request->sessionField][$request->field]);
}
// Session Field
if (!count($flash[$request->sessionField])) {
unset($flash[$request->sessionField]);
}
// If there's anything left to restore in the flash object, do so
if (count($flash)) {
$this->admin->session()->setFlashObject('files-upload', $flash);
}
$this->admin->json_response = ['status' => 'success'];
return true;
}
/**
* Handles form and saves the input data if its valid.
*
@@ -1758,8 +1814,6 @@ class AdminController
$obj = $obj->move($parent);
$this->preparePage($obj, false, $obj->language());
$obj = $this->processFiles($obj);
// Reset slug and route. For now we do not support slug twig variable on save.
$obj->slug($original_slug);
@@ -1792,7 +1846,7 @@ class AdminController
} else {
// Handle standard data types.
$obj = $this->prepareData($data);
$obj = $this->processFiles($obj);
try {
$obj->validate();
} catch (\Exception $e) {
@@ -1804,6 +1858,42 @@ class AdminController
$obj->filter();
}
// Process previously uploaded files for the current URI
// and finally store them. Everything else will get discarded
$queue = $this->admin->session()->getFlashObject('files-upload');
$queue = $queue[base64_encode($this->uri)];
if (is_array($queue)) {
foreach ($queue as $key => $files) {
foreach ($files as $destination => $file) {
if (!rename($file['tmp_name'], $destination)) {
throw new \RuntimeException(sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null, true), '"' . $file['tmp_name'] . '"', $destination));
}
unset($files[$destination]['tmp_name']);
}
if ($this->view == 'pages') {
$keys = explode('.', preg_replace('/^header./', '', $key));
$init_key = array_shift($keys);
if (count($keys) > 0) {
$new_data = isset($obj->header()->$init_key) ? $obj->header()->$init_key : [];
Utils::setDotNotation($new_data, implode('.', $keys), $files, true);
} else {
$new_data = $files;
}
if (isset($data['header'][$init_key])) {
$obj->modifyHeader($init_key, array_merge([], $data['header'][$init_key], $new_data));
} else {
$obj->modifyHeader($init_key, $new_data);
}
} else {
// TODO: [this is JS handled] if it's single file, remove existing and use set, if it's multiple, use join
$obj->join($key, $files); // stores
}
}
}
if ($obj) {
// Event to manipulate data before saving the object
$this->grav->fireEvent('onAdminSave', new Event(['object' => &$obj]));
@@ -2227,7 +2317,7 @@ class AdminController
return false;
}
$filename = base64_decode($this->route);
$filename = base64_decode($this->grav['uri']->param('route'));
$file = File::instance($filename);
$resultRemoveMedia = false;
@@ -2247,14 +2337,20 @@ class AdminController
}
if ($resultRemoveMedia && $resultRemoveMediaMeta) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL'), 'info');
} else {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.REMOVE_FAILED'), 'error');
}
$this->post = ['_redirect' => 'media'];
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
];
return true;
} else {
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_FAILED')
];
return false;
}
}
/**
@@ -2315,16 +2411,11 @@ class AdminController
break;
}
}
//
//
$redirect = base64_decode($uri->param('redirect'));
$route = $this->grav['config']->get('plugins.admin.route');
if (substr($redirect, 0, strlen($route)) == $route) {
$redirect = substr($redirect, strlen($route) + 1);
}
$this->post = ['_redirect' => $redirect];
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
];
return true;
}
@@ -2342,7 +2433,7 @@ class AdminController
// Decode JSON encoded fields and merge them to data.
if (isset($post['_json'])) {
$post = array_merge_recursive($post, $this->jsonDecode($post['_json']));
$post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
unset($post['_json']);
}
@@ -2532,4 +2623,29 @@ class AdminController
return true;
}
/**
* Internal method to normalize the $_FILES array
*
* @param array $data $_FILES starting point data
* @param string $key
* @return object a new Object with a normalized list of files
*/
protected function normalizeFiles($data, $key = '') {
$files = new \stdClass();
$files->field = $key;
$files->file = new \stdClass();
foreach($data as $fieldName => $fieldValue) {
// Since Files Upload are always happening via Ajax
// we are not interested in handling `multiple="true"`
// because they are always handled one at a time.
// For this reason we normalize the value to string,
// in case it is arriving as an array.
$value = (array) Utils::getDotNotation($fieldValue, $key);
$files->file->{$fieldName} = array_shift($value);
}
return $files;
}
}

View File

@@ -590,3 +590,16 @@ PLUGIN_ADMIN:
SESSION_PATH_HELP: "Use only if you choose a custom base URL (you rewrite the site domain / subfolder)"
CUSTOM_BASE_URL: "Custom base URL"
CUSTOM_BASE_URL_HELP: "Use if you want to rewrite the site domain or use a different subfolder than the one used by Grav. Example: http://localhost"
FILEUPLOAD_PREVENT_SELF: 'Cannot use "%s" outside of pages.'
FILEUPLOAD_UNABLE_TO_UPLOAD: 'Unable to upload file %s: %s'
FILEUPLOAD_UNABLE_TO_MOVE: 'Unable to move file %s to "%s"'
DROPZONE_CANCEL_UPLOAD: 'Cancel upload'
DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Are you sure you want to cancel this upload?'
DROPZONE_DEFAULT_MESSAGE: 'Drop your files here or <strong>click in this area</strong>'
DROPZONE_FALLBACK_MESSAGE: 'Your browser does not support drag and drop file uploads.'
DROPZONE_FALLBACK_TEXT: 'Please use the fallback form below to upload your files like in the olden days.'
DROPZONE_FILE_TOO_BIG: 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.'
DROPZONE_INVALID_FILE_TYPE: "You can't upload files of this type."
DROPZONE_MAX_FILES_EXCEEDED: "You can not upload any more files."
DROPZONE_REMOVE_FILE: "Remove file"
DROPZONE_RESPONSE_ERROR: "Server responded with {{statusCode}} code."

View File

@@ -157,7 +157,7 @@ export default class CollectionsField {
item.attr('data-collection-key', hasCustomKey ? observedValue : index);
['name', 'data-grav-field-name', 'for', 'id'].forEach((prop) => {
['name', 'data-grav-field-name', 'for', 'id', 'data-grav-file-settings'].forEach((prop) => {
item.find('[' + prop + '], [_' + prop + ']').each(function() {
let element = $(this);
let indexes = [];
@@ -180,12 +180,15 @@ export default class CollectionsField {
element.parents('[data-collection-key]').map((idx, parent) => indexes.push($(parent).attr('data-collection-key')));
indexes.reverse();
let matchedKey = currentKey;
let replaced = element.attr(prop).replace(regexps[0], (/* str, p1, offset */) => {
return `[${indexes.shift() || currentKey}]`;
matchedKey = indexes.shift() || matchedKey;
return `[${matchedKey}]`;
});
replaced = replaced.replace(regexps[1], (/* str, p1, offset */) => {
return `.${indexes.shift()}.`;
matchedKey = indexes.shift() || matchedKey;
return `.${matchedKey}.`;
});
element.attr(prop, replaced);

View File

@@ -1,22 +1,281 @@
import $ from 'jquery';
import format from '../../utils/formatbytes';
import Dropzone from 'dropzone';
import request from '../../utils/request';
import { config, translations } from 'grav-config';
$('body').on('change', '.form-input-file > input[type="file"]', (event) => {
let input = event.target;
let files = input.files;
let container = $(input).next();
// translations
const Dictionary = {
dictCancelUpload: translations.PLUGIN_ADMIN.DROPZONE_CANCEL_UPLOAD,
dictCancelUploadConfirmation: translations.PLUGIN_ADMIN.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
dictDefaultMessage: translations.PLUGIN_ADMIN.DROPZONE_DEFAULT_MESSAGE,
dictFallbackMessage: translations.PLUGIN_ADMIN.DROPZONE_FALLBACK_MESSAGE,
dictFallbackText: translations.PLUGIN_ADMIN.DROPZONE_FALLBACK_TEXT,
dictFileTooBig: translations.PLUGIN_ADMIN.DROPZONE_FILE_TOO_BIG,
dictInvalidFileType: translations.PLUGIN_ADMIN.DROPZONE_INVALID_FILE_TYPE,
dictMaxFilesExceeded: translations.PLUGIN_ADMIN.DROPZONE_MAX_FILES_EXCEEDED,
dictRemoveFile: translations.PLUGIN_ADMIN.DROPZONE_REMOVE_FILE,
dictResponseError: translations.PLUGIN_ADMIN.DROPZONE_RESPONSE_ERROR
};
if (files.length) {
let plural = files.length > 1 ? 's' : '';
let html = '';
html += `${files.length} file${plural} selected`;
html += '<ul>';
for (let i = 0; i < files.length; i++) {
html += `<li>${files[i].name} (${format(files[i].size, 2)})</li>`;
Dropzone.autoDiscover = false;
Dropzone.options.gravPageDropzone = {};
Dropzone.confirm = (question, accepted, rejected) => {
let doc = $(document);
let modalSelector = '[data-remodal-id="delete-media"]';
let removeEvents = () => {
doc.off('confirmation', modalSelector, accept);
doc.off('cancellation', modalSelector, reject);
$(modalSelector).find('.remodal-confirm').removeClass('pointer-events-disabled');
};
let accept = () => {
accepted && accepted();
removeEvents();
};
let reject = () => {
rejected && rejected();
removeEvents();
};
$.remodal.lookup[$(modalSelector).data('remodal')].open();
doc.on('confirmation', modalSelector, accept);
doc.on('cancellation', modalSelector, reject);
};
const DropzoneMediaConfig = {
createImageThumbnails: { thumbnailWidth: 150 },
addRemoveLinks: false,
dictDefaultMessage: translations.PLUGIN_ADMIN.DROP_FILES_HERE_TO_UPLOAD,
dictRemoveFileConfirmation: '[placeholder]',
previewTemplate: `
<div class="dz-preview dz-file-preview">
<div class="dz-details">
<div class="dz-filename"><span data-dz-name></span></div>
<div class="dz-size" data-dz-size></div>
<img data-dz-thumbnail />
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-success-mark"><span>✔</span></div>
<div class="dz-error-mark"><span>✘</span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<a class="dz-remove file-thumbnail-remove" href="javascript:undefined;" data-dz-remove><i class="fa fa-fw fa-close"></i></a>
</div>`.trim()
};
export default class FilesField {
constructor({ container = '.dropzone.files-upload', options = {} } = {}) {
this.container = $(container);
if (!this.container.length) { return; }
this.urls = {};
this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
klass: this,
url: this.container.data('file-url-add') || config.current_url,
acceptedFiles: this.container.data('media-types'),
init: this.initDropzone
}, this.container.data('dropzone-options'), options);
this.dropzone = new Dropzone(container, this.options);
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
this.dropzone.on('error', this.onDropzoneError.bind(this));
}
html += '</ul>';
initDropzone() {
let files = this.options.klass.container.find('[data-file]');
let dropzone = this;
if (!files.length) { return; }
container.html(html);
files.each((index, file) => {
file = $(file);
let data = file.data('file');
let mock = {
name: data.name,
size: data.size,
type: data.type,
status: Dropzone.ADDED,
accepted: true,
url: this.options.url,
removeUrl: data.remove
};
dropzone.files.push(mock);
dropzone.options.addedfile.call(dropzone, mock);
if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path);
file.remove();
});
}
});
onDropzoneSending(file, xhr, formData) {
formData.append('name', this.options.dotNotation);
formData.append('admin-nonce', config.admin_nonce);
formData.append('task', 'filesupload');
}
onDropzoneSuccess(file, response, xhr) {
if (this.options.reloadPage) {
global.location.reload();
}
// store params for removing file from session before it gets saved
if (response.session) {
file.sessionParams = response.session;
file.removeUrl = this.options.url;
// Touch field value to force a mutation detection
const input = this.container.find('[name][type="hidden"]');
const value = input.val();
input.val(value + ' ');
}
return this.handleError({
file,
data: response,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
<pre>${response.message}</pre>`
});
}
onDropzoneComplete(file) {
if (!file.accepted && !file.rejected) {
let data = {
status: 'error',
message: `${translations.PLUGIN_ADMIN.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
};
return this.handleError({
file,
data,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
<pre>${data.message}</pre>`
});
}
if (this.options.reloadPage) {
global.location.reload();
}
}
onDropzoneRemovedFile(file, ...extra) {
if (!file.accepted || file.rejected) { return; }
let url = file.removeUrl || this.urls.delete;
let path = (url || '').match(/path:(.*)\//);
let body = { filename: file.name };
if (file.sessionParams) {
body.task = 'filessessionremove';
body.session = file.sessionParams;
}
request(url, { method: 'post', body }, () => {
if (!path) { return; }
path = global.atob(path[1]);
let input = this.container.find('[name][type="hidden"]');
let data = JSON.parse(input.val() || '{}');
delete data[path];
input.val(JSON.stringify(data));
});
}
onDropzoneError(file, response, xhr) {
let message = xhr ? response.error.message : response;
$(file.previewElement).find('[data-dz-errormessage]').html(message);
return this.handleError({
file,
data: { status: 'error' },
msg: `<pre>${message}</pre>`
});
}
handleError(options) {
let { file, data, mode, msg } = options;
if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
switch (mode) {
case 'addBack':
if (file instanceof File) {
this.dropzone.addFile.call(this.dropzone, file);
} else {
this.dropzone.files.push(file);
this.dropzone.options.addedfile.call(this.dropzone, file);
this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
}
break;
case 'removeFile':
default:
if (~this.dropzone.files.indexOf(file)) {
file.rejected = true;
this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
}
break;
}
let modal = $('[data-remodal-id="generic"]');
modal.find('.error-content').html(msg);
$.remodal.lookup[modal.data('remodal')].open();
}
}
export function UriToMarkdown(uri) {
uri = uri.replace(/@3x|@2x|@1x/, '');
uri = uri.replace(/\(/g, '%28');
uri = uri.replace(/\)/g, '%29');
return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
}
let instances = [];
let cache = $();
const onAddedNodes = (event, target/* , record, instance */) => {
let files = $(target).find('.dropzone.files-upload');
if (!files.length) { return; }
files.each((index, file) => {
file = $(file);
if (!~cache.index(file)) {
addNode(file);
}
});
};
const addNode = (container) => {
container = $(container);
let input = container.find('input[type="file"]');
let settings = container.data('grav-file-settings') || {};
if (settings.accept && ~settings.accept.indexOf('*')) {
settings.accept = [''];
}
let options = {
url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
paramName: settings.paramName || 'file',
dotNotation: settings.name || 'file',
acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
maxFilesize: settings.filesize || 256,
maxFiles: settings.limit || null
};
cache = cache.add(container);
container = container[0];
instances.push(new FilesField({ container, options }));
};
export let Instances = (() => {
$('.dropzone.files-upload').each((i, container) => addNode(container));
$('body').on('mutation._grav', onAddedNodes);
return instances;
})();

View File

@@ -4,7 +4,7 @@ import CollectionsField, { Instance as CollectionsFieldInstance } from './collec
import DateTimeField, { Instance as DateTimeFieldInstance } from './datetime';
import EditorField, { Instance as EditorFieldInstance } from './editor';
import ColorpickerField, { Instance as ColorpickerFieldInstance } from './colorpicker';
import './files';
import FilesField, { Instance as FilesFieldInstance } from './files';
export default {
SelectizeField: {
@@ -30,5 +30,9 @@ export default {
ColorpickerField: {
ColorpickerField,
Instance: ColorpickerFieldInstance
},
FilesField: {
FilesField,
Instance: FilesFieldInstance
}
};

View File

@@ -1,43 +1,10 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
import request from '../../utils/request';
import FilesField, { UriToMarkdown } from '../../forms/fields/files';
import { config, translations } from 'grav-config';
import { Instance as Editor } from '../../forms/fields/editor';
Dropzone.autoDiscover = false;
Dropzone.options.gravPageDropzone = {};
Dropzone.confirm = (question, accepted, rejected) => {
let doc = $(document);
let modalSelector = '[data-remodal-id="delete-media"]';
let removeEvents = () => {
doc.off('confirmation', modalSelector, accept);
doc.off('cancellation', modalSelector, reject);
$(modalSelector).find('.remodal-confirm').removeClass('pointer-events-disabled');
};
let accept = () => {
accepted && accepted();
removeEvents();
};
let reject = () => {
rejected && rejected();
removeEvents();
};
$.remodal.lookup[$(modalSelector).data('remodal')].open();
doc.on('confirmation', modalSelector, accept);
doc.on('cancellation', modalSelector, reject);
};
const DropzoneMediaConfig = {
createImageThumbnails: { thumbnailWidth: 150 },
addRemoveLinks: false,
dictDefaultMessage: translations.PLUGIN_ADMIN.DROP_FILES_HERE_TO_UPLOAD,
dictRemoveFileConfirmation: '[placeholder]',
previewTemplate: `
const previewTemplate = `
<div class="dz-preview dz-file-preview">
<div class="dz-details">
<div class="dz-filename"><span data-dz-name></span></div>
@@ -50,26 +17,21 @@ const DropzoneMediaConfig = {
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<a class="dz-remove" href="javascript:undefined;" data-dz-remove>${translations.PLUGIN_ADMIN.DELETE}</a>
<a class="dz-insert" href="javascript:undefined;" data-dz-insert>${translations.PLUGIN_ADMIN.INSERT}</a>
</div>`.trim()
};
</div>`.trim();
export default class PageMedia {
constructor({form = '[data-media-url]', container = '#grav-dropzone', options = {}} = {}) {
this.form = $(form);
this.container = $(container);
if (!this.form.length || !this.container.length) { return; }
export default class PageMedia extends FilesField {
constructor({ container = '#grav-dropzone', options = {} } = {}) {
options = Object.assign(options, { previewTemplate });
super({ container, options });
if (!this.container.length) { return; }
this.options = Object.assign({}, DropzoneMediaConfig, {
url: `${this.form.data('media-url')}/task${config.param_sep}addmedia`,
acceptedFiles: this.form.data('media-types')
}, this.form.data('dropzone-options'), options);
this.urls = {
fetch: `${this.container.data('media-url')}/task${config.param_sep}listmedia`,
add: `${this.container.data('media-url')}/task${config.param_sep}addmedia`,
delete: `${this.container.data('media-url')}/task${config.param_sep}delmedia`
};
this.dropzone = new Dropzone(container, this.options);
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
this.dropzone.on('error', this.onDropzoneError.bind(this));
this.dropzone.options.url = this.urls.add;
if (typeof this.options.fetchMedia === 'undefined' || this.options.fetchMedia) {
this.fetchMedia();
@@ -81,9 +43,9 @@ export default class PageMedia {
}
fetchMedia() {
let url = `${this.form.data('media-url')}/task${config.param_sep}listmedia/admin-nonce${config.param_sep}${config.admin_nonce}`;
let url = this.urls.fetch;
request(url, (response) => {
request(url, { method: 'post' }, (response) => {
let results = response.results;
Object.keys(results).forEach((name) => {
@@ -103,96 +65,15 @@ export default class PageMedia {
}
onDropzoneSending(file, xhr, formData) {
formData.append('admin-nonce', config.admin_nonce);
}
onDropzoneSuccess(file, response, xhr) {
if (this.options.reloadPage) {
global.location.reload();
}
return this.handleError({
file,
data: response,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
<pre>${response.message}</pre>`
});
super.onDropzoneSending(file, xhr, formData);
formData.delete('task');
}
onDropzoneComplete(file) {
if (!file.accepted) {
let data = {
status: 'error',
message: `${translations.PLUGIN_ADMIN.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
};
return this.handleError({
file,
data,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
<pre>${data.message}</pre>`
});
}
super.onDropzoneComplete(file);
// accepted
$('.dz-preview').prop('draggable', 'true');
if (this.options.reloadPage) {
global.location.reload();
}
}
onDropzoneRemovedFile(file, ...extra) {
if (!file.accepted || file.rejected) { return; }
let url = `${this.form.data('media-url')}/task${config.param_sep}delmedia`;
request(url, {
method: 'post',
body: {
filename: file.name
}
});
}
onDropzoneError(file, response, xhr) {
let message = xhr ? response.error.message : response;
$(file.previewElement).find('[data-dz-errormessage]').html(message);
return this.handleError({
file,
data: { status: 'error' },
msg: `<pre>${message}</pre>`
});
}
handleError(options) {
let { file, data, mode, msg } = options;
if (data.status !== 'error' && data.status !== 'unauthorized') { return ; }
switch (mode) {
case 'addBack':
if (file instanceof File) {
this.dropzone.addFile.call(this.dropzone, file);
} else {
this.dropzone.files.push(file);
this.dropzone.options.addedfile.call(this.dropzone, file);
this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
}
break;
case 'removeFile':
file.rejected = true;
this.dropzone.removeFile.call(this.dropzone, file);
break;
default:
}
let modal = $('[data-remodal-id="generic"]');
modal.find('.error-content').html(msg);
$.remodal.lookup[modal.data('remodal')].open();
}
attachDragDrop() {
@@ -228,13 +109,5 @@ export default class PageMedia {
}
}
export function UriToMarkdown(uri) {
uri = uri.replace(/@3x|@2x|@1x/, '');
uri = uri.replace(/\(/g, '%28');
uri = uri.replace(/\)/g, '%29');
return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
}
export let Instance = new PageMedia();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -61,11 +61,12 @@ var compileJS = function(watch) {
.pipe(gulpWebpack(prodOpts.toJS()))
.pipe(gulp.dest('js/'));
var dev = gulp.src('app/main.js')
/*var dev = gulp.src('app/main.js')
.pipe(gulpWebpack(devOpts.toJS()))
.pipe(gulp.dest('js/'));
.pipe(gulp.dest('js/'));*/
return merge(prod, dev);
// return merge(prod, dev);
return prod;
};
var compileCSS = function(event) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -701,7 +701,7 @@ form {
}
.form-input-file {
border: 4px dashed $form-border;
border: 2px dashed $form-border;
p {
color: $form-field-text;
@@ -736,6 +736,9 @@ form {
}
.form-order-wrapper {
.note {
background: inherit;
}
ul#ordering {
li {
border: 1px solid $form-border;
@@ -1171,7 +1174,7 @@ form {
color: $critical-bg;
}
&:hover .dz-remove, &:hover .dz-insert {
&:hover .dz-remove:not(.file-thumbnail-remove), &:hover .dz-insert {
border:1px solid darken($content-bg,5%);
&:hover {
background: $content-bg;

View File

@@ -1,7 +1,7 @@
/* The MIT License */
$preview-width: 150px;
$preview-height: 100px;
$preview-height: 150px;
.dropzone {
position: relative;
@@ -121,7 +121,7 @@ $preview-height: 100px;
display: none;
}
&:hover .dz-remove, &:hover .dz-insert {
&:hover .dz-remove:not(.file-thumbnail-remove), &:hover .dz-insert {
display: block;
position: absolute;
left: 0;
@@ -134,9 +134,15 @@ $preview-height: 100px;
}
&:hover .dz-remove {
&.file-thumbnail-remove {
display: block;
}
&:not(.file-thumbnail-remove) {
left: inherit;
border-left: 0;
}
}
&:hover .dz-insert {
right: inherit;
@@ -207,8 +213,9 @@ $preview-height: 100px;
.dz-message, .dz-message span {
cursor: pointer;
text-align: center;
font-size: 1.4rem;
line-height: 4rem;
font-size: 1.2rem;
line-height: 1.4;
margin: 1rem 0;
}
}
* {

View File

@@ -158,13 +158,7 @@ form {
}
input {
position: absolute;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
outline: none;
opacity: 0;
display: none;
}
}

View File

@@ -1,9 +1,9 @@
{% extends "forms/field.html.twig" %}
{% set value = (value is null ? field.default : value) %}
{% set default_php_dateformat = admin.guessDateFormat(value) %}
{% set php_dateformat = admin.page.dateformat ?: config.system.pages.dateformat.default ?: default_php_dateformat %}
{% set js_dateformat = admin.dateformatToMomentJS(php_dateformat) %}
{% set value = (value is null ? field.default : value) %}
{% set value = (value is null ? value : value|date(php_dateformat)) %}
{% block input %}

View File

@@ -1,6 +1,28 @@
{% extends "forms/field.html.twig" %}
{% set defaults = config.plugins.form %}
{% set files = defaults.files|merge(field|default([])) %}
{% set limit = not field.multiple ? 1 : files.limit %}
{% macro bytesToSize(bytes) -%}
{% spaceless %}
{% set kilobyte = 1024 %}
{% set megabyte = kilobyte * 1024 %}
{% set gigabyte = megabyte * 1024 %}
{% set terabyte = gigabyte * 1024 %}
{% if bytes < kilobyte %}
{{ bytes ~ ' B' }}
{% elseif bytes < megabyte %}
{{ (bytes / kilobyte)|number_format(2, '.') ~ ' KB' }}
{% elseif bytes < gigabyte %}
{{ (bytes / megabyte)|number_format(2, '.') ~ ' MB' }}
{% elseif bytes < terabyte %}
{{ (bytes / gigabyte)|number_format(2, '.') ~ ' GB' }}
{% else %}
{{ (bytes / terabyte)|number_format(2, '.') ~ ' TB' }}
{% endif %}
{% endspaceless %}
{%- endmacro %}
{% macro preview(path, value, global) %}
{% if value %}
@@ -8,31 +30,25 @@
{% set files = global.files %}
{% set config = global.grav.config %}
{% set route = global.context.route() %}
{% set type = global.context.content() ? 'pages' : global.plugin ? 'plugins' : global.theme ? 'themes' : 'config' %}
{% set type = global.context.content() is not null ? 'pages' : global.plugin ? 'plugins' : global.theme ? 'themes' : 'config' %}
{% set blueprint_name = global.blueprints.getFilename %}
{% if type == 'pages' %}
{% set blueprint_name = type ~ '/' ~ blueprint_name %}
{% endif %}
{% set blueprint = base64_encode(blueprint_name) %}
{% set real_path = global.admin.getPagePathFromToken(path) %}
{% if type == 'pages' %}
{% include 'forms/fields/hidden/hidden.html.twig' with {field: {name: 'data._json.' ~ global.field.name}, value:{(value.path):value}|raw|json_encode} %}
{% endif %}
<div class="file-thumbnail-wrapper">
<img class="file-thumbnail" src="{{ uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/'}}{{ real_path }}" alt="{{ path|replace({(files.destination ~ '/'): ''}) }}" />
<a class="file-thumbnail-remove" href="{{ uri.addNonce(global.base_url_relative ~
'/media/' ~ base64_encode(global.base_path ~ '/' ~ real_path) ~
{% set remove = uri.addNonce(global.base_url_relative ~
'/media.json' ~
'/route' ~ config.system.param_sep ~ base64_encode(global.base_path ~ '/' ~ real_path) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/proute' ~ config.system.param_sep ~ base64_encode(route) ~
'/blueprint' ~ config.system.param_sep ~ blueprint ~
'/type' ~ config.system.param_sep ~ type ~
'/field' ~ config.system.param_sep ~ files.name ~
'/path' ~ config.system.param_sep ~ base64_encode(value.path) ~
'/redirect' ~ config.system.param_sep ~ base64_encode(uri.path), 'admin-form', 'admin-nonce') }}">
<i class="fa fa-fw fa-close"></i>
</a>
</div>
'/path' ~ config.system.param_sep ~ base64_encode(value.path), 'admin-form', 'admin-nonce') %}
{% set file = value|merge({remove: remove, path: (uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/' ~ real_path) }) %}
<div class="hidden" data-file="{{ file|json_encode|e('html_attr') }}"></div>
{% endif %}
{% endmacro %}
@@ -40,14 +56,11 @@
{% set page_can_upload = exists or (type == 'page' and not exists and not (field.destination starts with '@self' or field.destination starts with 'self@')) %}
{% if type is not defined or page_can_upload %}
{% for path, file in value %}
{{ _self.preview(path, file, _context) }}
{% endfor %}
{% set settings = {name: field.name, paramName: (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : ''), limit: limit, filesize: files.filesize, accept: files.accept} %}
<div class="form-input-wrapper {% if field.fancy is not same as(false) %}form-input-file{% endif %} {{ field.size|default('xlarge') }}">
<div class="form-input-wrapper dropzone files-upload {% if field.fancy is not same as(false) %}form-input-file{% endif %} {{ field.size|default('xlarge') }}" data-grav-file-settings="{{ settings|json_encode|e('html_attr') }}">
<input
{# required attribute structures #}
name="{{ (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : '') }}"
{% block input_attributes %}
type="file"
{% if files.multiple %}multiple="multiple"{% endif %}
@@ -57,9 +70,11 @@
{{ parent() }}
{% endblock %}
/>
{% if field.fancy is not same as(false) %}
<p>{{ "PLUGIN_ADMIN.DROP_FILES_HERE_TO_UPLOAD"|tu|raw }}</p>
{% endif %}
{% for path, file in value %}
{{ _self.preview(path, file, _context) }}
{% endfor %}
{% include 'forms/fields/hidden/hidden.html.twig' with {field: {name: '_json.' ~ field.name}, value:value|raw|json_encode} %}
</div>
{% else %}

View File

@@ -6,7 +6,13 @@
<label>{{ field.label|tu }}</label>
</div>
<div class="form-data form-uploads-wrapper">
<div id="grav-dropzone" class="dropzone"></div>
{% set uploadLimit = grav.config.system.media.upload_limit / 1024 / 1024 %}
{% set dropzoneSettings = { maxFileSize: uploadLimit } %}
<div id="grav-dropzone"
class="dropzone"
data-media-url="{{ base_url }}/media/{{ admin.route|trim('/') }}.json"
data-media-local="{{ base_url_relative_frontend|rtrim('/') }}/{{ admin.route|trim('/') }}"
data-dropzone-options="{{ dropzoneSettings|json_encode|e('html_attr') }}"></div>
<span>{{ value|join("\n") }}</span>
</div>
</div>

View File

@@ -204,10 +204,7 @@
{% if mode == 'new' %}
{% include 'partials/blueprints-new.html.twig' with { blueprints: admin.blueprints('pages/page'), data: context } %}
{% elseif mode == 'edit' %}
{% set uploadLimit = grav.config.system.media.upload_limit / 1024 / 1024 %}
{% set dropzoneSettings = { maxFileSize: uploadLimit } %}
<div class="admin-form-wrapper" data-media-url="{{ base_url }}/media/{{ admin.route|trim('/') }}.json" data-media-local="{{ base_url_relative_frontend|rtrim('/') }}/{{ admin.route|trim('/') }}" data-dropzone-options="{{ dropzoneSettings|json_encode|e('html_attr') }}">
<div class="admin-form-wrapper">
<div id="admin-topbar">
{% if admin.multilang and page_lang %}
@@ -281,16 +278,6 @@
{% endif %}
</div>
<div class="remodal" data-remodal-id="generic" data-remodal-options="hashTracking: false">
<form>
<h1>{{ "PLUGIN_ADMIN.ERROR"|tu }}</h1>
<div class="error-content"></div>
<div class="button-bar">
<a class="button remodal-cancel" data-remodal-action="cancel" href="#">{{ "PLUGIN_ADMIN.CLOSE"|tu }}</a>
</div>
</form>
</div>
{% if mode == 'list' %}
<div class="remodal" data-remodal-id="modal" data-remodal-options="hashTracking: false">
@@ -337,18 +324,4 @@
</div>
</form>
</div>
<div class="remodal" data-remodal-id="delete-media" data-remodal-options="hashTracking: false">
<form>
<h1>{{ "PLUGIN_ADMIN.MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE"|tu }}</h1>
<p class="bigger">
{{ "PLUGIN_ADMIN.MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC"|tu }}
</p>
<br>
<div class="button-bar">
<button data-remodal-action="cancel" class="button secondary remodal-cancel"><i class="fa fa-fw fa-close"></i> {{ "PLUGIN_ADMIN.CANCEL"|tu }}</button>
<button data-remodal-action="confirm" class="button remodal-confirm disable-after-click"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -68,6 +68,29 @@
</div>
</div>
<div class="remodal" data-remodal-id="generic" data-remodal-options="hashTracking: false">
<form>
<h1>{{ "PLUGIN_ADMIN.ERROR"|tu }}</h1>
<div class="error-content"></div>
<div class="button-bar">
<a class="button remodal-cancel" data-remodal-action="cancel" href="#">{{ "PLUGIN_ADMIN.CLOSE"|tu }}</a>
</div>
</form>
</div>
<div class="remodal" data-remodal-id="delete-media" data-remodal-options="hashTracking: false">
<form>
<h1>{{ "PLUGIN_ADMIN.MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE"|tu }}</h1>
<p class="bigger">
{{ "PLUGIN_ADMIN.MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC"|tu }}
</p>
<br>
<div class="button-bar">
<button data-remodal-action="cancel" class="button secondary remodal-cancel"><i class="fa fa-fw fa-close"></i> {{ "PLUGIN_ADMIN.CANCEL"|tu }}</button>
<button data-remodal-action="confirm" class="button remodal-confirm disable-after-click"><i class="fa fa-fw fa-check"></i> {{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
</div>
</form>
</div>
</main>
<div id='overlay'></div>
</div>

View File

@@ -2,6 +2,7 @@
<script type="text/javascript">
window.GravAdmin = window.GravAdmin || {};
window.GravAdmin.config = {
current_url: '{{ uri.route(true) }}',
base_url_relative: '{{ base_url_relative }}',
route: '{{ admin.route|trim('/') }}',
param_sep: '{{ config.system.param_sep }}',