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:
Flavio Copes
2016-09-02 18:29:47 +02:00
committed by GitHub
parent 6b34336599
commit 8a39b36603
9 changed files with 231 additions and 39 deletions

View File

@@ -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
*

View 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();

View File

@@ -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
}
};

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

@@ -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;
}

View File

@@ -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 %}