mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2026-07-25 23:51:30 +02:00
Filepicker form field (#750)
# What this PR introduces
This PR introduces a `filepicker` field which is a replacement for the `pagemediaselect` field.
This field loads the files list via AJAX each time it's triggered. This means you can now upload a file via FTP or via Admin and the new file is immediately available to be selected.
Previously, `pagemediaselect` required a page reload to see the newly added files.
--
### Options
#### `accept`
`accept` allows file extensions. For example, to only allow `yaml` and `json` files:
```yaml
accept:
- .yaml
- .json
```
By default, any file is listed.
#### `folder`
```yaml
folder: 'user/plugins/testing`
```
folder has been enhanced to allow `self@` as well as `page@:` and `theme@:` prefixes.
Example of usage, assuming we have a blog item at the route `/blog/ajax-upload` (physical location being `user/pages/02.blog/ajax-upload`), with the `page@:` prefix the folder would be:
```yaml
folder: 'page@:/blog/ajax-upload'
```
#### `preview_images`
```yaml
preview_images: true
```
If enabled, shows a preview for the images file types
> ### NOTE: `self@` is not allowed outside of the Pages scope, an error will be thrown.
## Example usage
```
header.a_file:
type: filepicker
folder: 'user/plugins/admin'
label: Select a file
```
This commit is contained in:
@@ -339,6 +339,56 @@ class AdminController
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the filepicker field to get a list of files in a folder.
|
||||
*/
|
||||
protected function taskGetFilesInFolder()
|
||||
{
|
||||
if (!$this->authorizeTask('save', $this->dataPermissions())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->view == 'pages' ? $this->admin->page(true) : $this->prepareData([]);
|
||||
|
||||
$settings = $data->blueprints()->schema()->getProperty($this->post['name']);
|
||||
|
||||
$folder = $settings['folder'];
|
||||
|
||||
// Do not use self@ outside of pages
|
||||
if ($this->view != 'pages' && in_array($folder, ['@self', 'self@'])) {
|
||||
$this->admin->json_response = [
|
||||
'status' => 'error',
|
||||
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null, true), $folder)
|
||||
];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set destination
|
||||
$folder = Folder::getRelativePath(rtrim($folder, '/'));
|
||||
$folder = $this->admin->getPagePathFromToken($folder);
|
||||
|
||||
$available_files = Folder::all($folder, ['recursive' => false]);
|
||||
|
||||
// Handle Accepted file types
|
||||
// Accept can only be file extensions (.pdf|.jpg)
|
||||
$accepted_files = [];
|
||||
if (!isset($settings['accept'])) {
|
||||
$accepted_files = $available_files;
|
||||
} else {
|
||||
foreach ((array) $available_files as $available_file) {
|
||||
foreach ((array) $settings['accept'] as $type) {
|
||||
$find = str_replace('*', '.*', $type);
|
||||
$match = preg_match('#' . $find . '$#', $available_file);
|
||||
if ($match) {
|
||||
$accepted_files[] = $available_file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->admin->json_response = ['status' => 'success', 'files' => $accepted_files, 'folder' => $folder];
|
||||
}
|
||||
|
||||
protected function taskGetNewsFeed()
|
||||
{
|
||||
$cache = $this->grav['cache'];
|
||||
@@ -1697,7 +1747,7 @@ class AdminController
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes a file from the flash object session, before it gets saved
|
||||
*
|
||||
|
||||
117
themes/grav/app/forms/fields/filepicker.js
Normal file
117
themes/grav/app/forms/fields/filepicker.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import $ from 'jquery';
|
||||
import { config } from 'grav-config';
|
||||
import request from '../../utils/request';
|
||||
|
||||
export default class FilePickerField {
|
||||
|
||||
constructor(options) {
|
||||
$('[data-grav-filepicker]').each((index, element) => this.add(element));
|
||||
}
|
||||
|
||||
add(element) {
|
||||
element = $(element);
|
||||
let tag = element.prop('tagName').toLowerCase();
|
||||
let isInput = tag === 'input' || tag === 'select';
|
||||
|
||||
let field = (isInput ? element : element.find('input, select'));
|
||||
|
||||
var folder = '';
|
||||
|
||||
if (!field.length || field.get(0).selectize) { return; }
|
||||
|
||||
var getData = function getData(field, callback) {
|
||||
let url = config.current_url + `.json/task${config.param_sep}getFilesInFolder`;
|
||||
let parent = field.first().parents('[data-grav-filepicker]');
|
||||
let name = parent.data('name');
|
||||
let value = parent.data('value');
|
||||
|
||||
request(url, {
|
||||
method: 'post',
|
||||
body: {
|
||||
name: name
|
||||
}
|
||||
}, (response) => {
|
||||
if (typeof response.files === 'undefined') {
|
||||
return;
|
||||
}
|
||||
var data = [];
|
||||
for (var i = 0; i < response.files.length; i++) {
|
||||
data.push({'name': response.files[i]});
|
||||
}
|
||||
|
||||
folder = response.folder;
|
||||
|
||||
callback(data, value);
|
||||
});
|
||||
};
|
||||
|
||||
var refreshingCurrentValue = false;
|
||||
var preview_images = false;
|
||||
if (field.first().parents('[data-preview-images]').length > 0) {
|
||||
preview_images = true;
|
||||
}
|
||||
|
||||
var renderOption = function renderOption(item, escape) {
|
||||
let image = '';
|
||||
if (preview_images) {
|
||||
if (item.name.match(/\.(jpg|jpeg|png|gif)$/)) {
|
||||
image = '<img class="filepicker-field-image" src="' + config.base_url_relative + '/../' + folder + '/' + item.name + '"/>';
|
||||
}
|
||||
}
|
||||
|
||||
return '<div>' +
|
||||
'<span class="title">' +
|
||||
image +
|
||||
'<span class="name filepicker-field-name">' + escape(item.name) + '</span>' +
|
||||
'</span>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
field.selectize({
|
||||
valueField: 'name',
|
||||
labelField: 'name',
|
||||
searchField: 'name',
|
||||
create: false,
|
||||
preload: true,
|
||||
render: {
|
||||
option: function(item, escape) {
|
||||
return renderOption(item, escape);
|
||||
},
|
||||
item: function(item, escape) {
|
||||
return renderOption(item, escape);
|
||||
}
|
||||
},
|
||||
load: function(query, callback) {
|
||||
var that = this;
|
||||
|
||||
getData(field, function(data, value) {
|
||||
callback(data);
|
||||
that.setValue(value);
|
||||
});
|
||||
},
|
||||
onFocus: function() {
|
||||
if (refreshingCurrentValue) {
|
||||
return;
|
||||
}
|
||||
var that = this;
|
||||
var currentValue = that.getValue();
|
||||
this.clearOptions();
|
||||
|
||||
getData(field, function(data, value) {
|
||||
data.forEach(function(item) {
|
||||
that.addOption(item);
|
||||
that.refreshOptions();
|
||||
});
|
||||
|
||||
refreshingCurrentValue = true;
|
||||
that.setValue(currentValue);
|
||||
setTimeout(function() {
|
||||
refreshingCurrentValue = false;
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new FilePickerField();
|
||||
@@ -1,3 +1,4 @@
|
||||
import FilepickerField, { Instance as FilepickerFieldInstance } from './filepicker';
|
||||
import SelectizeField, { Instance as SelectizeFieldInstance } from './selectize';
|
||||
import ArrayField, { Instance as ArrayFieldInstance } from './array';
|
||||
import CollectionsField, { Instance as CollectionsFieldInstance } from './collections';
|
||||
@@ -7,6 +8,10 @@ import ColorpickerField, { Instance as ColorpickerFieldInstance } from './colorp
|
||||
import FilesField, { Instance as FilesFieldInstance } from './files';
|
||||
|
||||
export default {
|
||||
FilepickerField: {
|
||||
FilepickerField,
|
||||
Instance: FilepickerFieldInstance
|
||||
},
|
||||
SelectizeField: {
|
||||
SelectizeField,
|
||||
Instance: SelectizeFieldInstance
|
||||
@@ -36,3 +41,4 @@ export default {
|
||||
Instance: FilesFieldInstance
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
2
themes/grav/css-compiled/template.css
vendored
2
themes/grav/css-compiled/template.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
30
themes/grav/js/admin.min.js
vendored
30
themes/grav/js/admin.min.js
vendored
File diff suppressed because one or more lines are too long
40
themes/grav/js/vendor.min.js
vendored
40
themes/grav/js/vendor.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -498,7 +498,7 @@ textarea.frontmatter {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[data-collection-nosort] .collection-sort {
|
||||
display: none;
|
||||
}
|
||||
@@ -565,3 +565,13 @@ textarea.frontmatter {
|
||||
.form-fieldset input:checked ~ .form-label .form-fieldset--collapsible .open {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filepicker-field-image {
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.filepicker-field-name {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% embed "forms/fields/select/select.html.twig" %}
|
||||
{% block global_attributes %}
|
||||
data-grav-filepicker
|
||||
data-name="{{field.name}}"
|
||||
{% if field.preview_images %}data-preview-images{% endif %}
|
||||
data-value="{{value}}"
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
Reference in New Issue
Block a user