1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 00:06:55 +02:00

Add support for custom fields in file/image fields. Details and instructions coming on Friday, but if you want to preview, create a template with name "field-images" where the "images" part is the name of your file/image field. Add the fields to it that you want to represent your custom fields, and it's ready to use. Supports most core Fieldtypes (including multi-language) with the following exceptions that are not supported as custom fields in an File/Image field: CKEditor, Repeaters, PageTable, Files (nested), Images (nested), Comments, Cache, Selector.

This commit is contained in:
Ryan Cramer
2019-10-09 12:01:19 -04:00
parent eae15ce88a
commit 573048abb4
13 changed files with 809 additions and 279 deletions

View File

@@ -12,7 +12,7 @@
* Pagefile objects are contained by a `Pagefiles` object. * Pagefile objects are contained by a `Pagefiles` object.
* #pw-body * #pw-body
* *
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer * ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @property-read string $url URL to the file on the server. * @property-read string $url URL to the file on the server.
@@ -23,6 +23,7 @@
* @property-read string $name Returns the filename without the path, same as the "basename" property. * @property-read string $name Returns the filename without the path, same as the "basename" property.
* @property-read string $hash Get a unique hash (for the page) representing this Pagefile. * @property-read string $hash Get a unique hash (for the page) representing this Pagefile.
* @property-read array $tagsArray Get file tags as an array. #pw-group-tags @since 3.0.17 * @property-read array $tagsArray Get file tags as an array. #pw-group-tags @since 3.0.17
* @property-read array $fieldValues Custom field values. #pw-internal @since 3.0.142
* @property int $sort Sort order in database. #pw-group-other * @property int $sort Sort order in database. #pw-group-other
* @property string $basename Returns the filename without the path. * @property string $basename Returns the filename without the path.
* @property string $description Value of the files description field (string), if enabled. Note you can also set this property directly. * @property string $description Value of the files description field (string), if enabled. Note you can also set this property directly.
@@ -77,6 +78,17 @@ class Pagefile extends WireData {
*/ */
protected $filedata = array(); protected $filedata = array();
/**
* Custom field values indexed by field name, loaded on request
*
* Values here have been run through wakeupValue and sanitizeValue already.
* Prior to that they are stored in $filedata (above).
*
* @var array
*
*/
protected $fieldValues = array();
/** /**
* Construct a new Pagefile * Construct a new Pagefile
* *
@@ -227,6 +239,8 @@ class Pagefile extends WireData {
$language = $languages->get((int) $matches[1]); $language = $languages->get((int) $matches[1]);
if($language && $language->id) return $this->setDescription($value, $language); if($language && $language->id) return $this->setDescription($value, $language);
} }
} else if($this->setFieldValue($key, $value)) {
return $this;
} }
return parent::set($key, $value); return parent::set($key, $value);
@@ -237,7 +251,6 @@ class Pagefile extends WireData {
* *
* Filedata is any additional data that you want to store with the files database record. * Filedata is any additional data that you want to store with the files database record.
* *
*
* - To get a value, specify just the $key argument. Null is returned if request value is not present. * - To get a value, specify just the $key argument. Null is returned if request value is not present.
* - To get all values, omit all arguments. An associative array will be returned. * - To get all values, omit all arguments. An associative array will be returned.
* - To set a value, specify the $key and the $value to set. * - To set a value, specify the $key and the $value to set.
@@ -558,11 +571,120 @@ class Pagefile extends WireData {
$value = filemtime($this->filename()); $value = filemtime($this->filename());
if(strpos($key, 'Str')) $value = wireDate($this->wire('config')->dateFormat, $value); if(strpos($key, 'Str')) $value = wireDate($this->wire('config')->dateFormat, $value);
break; break;
case 'fieldValues':
return $this->fieldValues;
break;
default:
$value = $this->getFieldValue($key);
} }
if(is_null($value)) return parent::get($key); if(is_null($value)) return parent::get($key);
return $value; return $value;
} }
/**
* Get a custom field value
*
* #pw-internal Most non-core cases should just use get() or direct access rather than this method
*
* @param string $name
* @param bool|null Get as formatted value? true=yes, false=no, null=use page output formatting setting (default=null)
* @return mixed|null Returns value or null if not present
* @since 3.0.142
*
*/
public function getFieldValue($name, $formatted = null) {
$field = $this->wire('fields')->get($name);
if(!$field) return null;
$template = $this->pagefiles->getFieldsTemplate();
if(!$template) return null;
$fieldgroup = $template->fieldgroup;
if(!$fieldgroup->hasField($field)) return null;
$field = $fieldgroup->getFieldContext($field); // get in context
$fieldtype = $field->type; /** @var Fieldtype $fieldtype */
$fileField = $this->pagefiles->getField(); /** @var Field $fileField */
$fileFieldtype = $fileField->type; /** @var FieldtypeFile|FieldtypeImage $fileFieldtype */
$page = $fileFieldtype->getFieldsPage($fileField);
if(array_key_exists($name, $this->fieldValues)) {
$value = $this->fieldValues[$name];
} else {
$idKey = "_$field->id";
$value = $this->filedata($idKey);
if($value !== null) {
$value = $fieldtype->wakeupValue($page, $field, $value);
$value = $fieldtype->sanitizeValue($page, $field, $value);
}
$this->fieldValues[$name] = $value;
unset($this->filedata[$idKey]); // avoid storing double copies
}
if($value === null) {
$value = $fieldtype->getBlankValue($page, $field);
$value = $fieldtype->sanitizeValue($page, $field, $value);
$this->fieldValues[$name] = $value;
}
if($formatted === null) $formatted = $this->page->of();
if($formatted) $value = $fieldtype->formatValue($page, $field, $value);
return $value;
}
/**
* Set a custom field value
*
* #pw-internal Most non-core cases should use set() instead
*
* @param string $name
* @param mixed $value
* @param bool|null $changed Specify true to force track change, false to force no change, or null to auto-detect (default=null)
* @return bool Returns true if value set, or false if not (like if theres no template defined for the purpose)
* @since 3.0.142
*
*
*/
public function setFieldValue($name, $value, $changed = null) {
$template = $this->pagefiles->getFieldsTemplate();
if(!$template) return false;
$fieldgroup = $template->fieldgroup;
if(!$fieldgroup) return false;
$field = $fieldgroup->getFieldContext($name);
if(!$field) return false;
$page = $this->pagefiles->getFieldsPage();
/** @var Fieldtype $fieldtype */
$fieldtype = $field->type;
$value = $fieldtype->sanitizeValue($page, $field, $value);
if($changed === null && $this->page->trackChanges()) {
// detect if a change has taken place
$oldValue = $this->getFieldValue($field->name, false);
if(is_object($oldValue) && $oldValue instanceof Wire && $oldValue === $value) {
// $oldValue and new $value are the same object instance, so ask it if anything has changed
$changed = $oldValue->isChanged();
if($changed) $this->trackChange($field->name);
} else if($oldValue != $value) {
// $oldValue and new $value differ, record change
$this->trackChange($field->name, $oldValue, $value);
}
} else if($changed === true) {
$this->trackChange($field->name);
}
$this->fieldValues[$field->name] = $value;
return true;
}
/** /**
* Hookable no-cache URL * Hookable no-cache URL
* *

View File

@@ -109,6 +109,12 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*/ */
protected $formatted = false; protected $formatted = false;
/**
* @var Template|null
*
*/
protected $fieldsTemplate = null;
/** /**
* Construct a Pagefiles object * Construct a Pagefiles object
* *
@@ -117,6 +123,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*/ */
public function __construct(Page $page) { public function __construct(Page $page) {
$this->setPage($page); $this->setPage($page);
parent::__construct();
} }
/** /**
@@ -917,6 +924,44 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return $this->formatted; return $this->formatted;
} }
/**
* Get Template object used for Pagefile custom fields, if available (false if not)
*
* #pw-internal
*
* @return bool|Template
* @since 3.0.142
*
*/
public function getFieldsTemplate() {
if($this->fieldsTemplate === null) {
/** @var Field $field */
$field = $this->getField();
if($field) {
$this->fieldsTemplate = false;
/** @var FieldtypeFile $fieldtype */
$fieldtype = $field->type;
$template = $fieldtype ? $fieldtype->getFieldsTemplate($field) : null;
if($template) $this->fieldsTemplate = $template;
}
}
return $this->fieldsTemplate;
}
/**
* Get mock/placeholder Page object used for Pagefile custom fields
*
* @return Page
* @since 3.0.142
*
*/
public function getFieldsPage() {
$field = $this->getField();
/** @var FieldtypeFile $fieldtype */
$fieldtype = $field->type;
return $fieldtype->getFieldsPage($field);
}
/** /**
* Debug info * Debug info
* *

View File

@@ -9,19 +9,20 @@
* /wire/core/Fieldtype.php * /wire/core/Fieldtype.php
* /wire/core/FieldtypeMulti.php * /wire/core/FieldtypeMulti.php
* *
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer * ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @property array $allowFieldtypes Allowed Fieldtype types for custom fields
* @method string formatValueString(Page $page, Field $field, $value) * @method string formatValueString(Page $page, Field $field, $value)
* *
*/ */
class FieldtypeFile extends FieldtypeMulti { class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
public static function getModuleInfo() { public static function getModuleInfo() {
return array( return array(
'title' => __('Files', __FILE__), 'title' => __('Files', __FILE__),
'version' => 105, 'version' => 106,
'summary' => __('Field that stores one or more files', __FILE__), 'summary' => __('Field that stores one or more files', __FILE__),
'permanent' => true, 'permanent' => true,
); );
@@ -95,12 +96,40 @@ class FieldtypeFile extends FieldtypeMulti {
*/ */
protected $defaultInputfieldClass = ''; protected $defaultInputfieldClass = '';
/**
* Default fieldtypes allowed for custom fields
*
* @var array
*
*/
protected $defaultAllowFieldtypes = array(
'Checkbox',
'Datetime',
'Email',
'FieldsetClose',
'FieldsetOpen',
'Float',
'Integer',
'Page',
'PageTitle',
'PageTitleLanguage',
'Text',
'TextLanguage',
'Textarea',
'TextareaLanguage',
'Toggle',
'URL',
);
/** /**
* Construct * Construct
* *
*/ */
public function __construct() { public function __construct() {
$this->defaultInputfieldClass = str_replace('Fieldtype', 'Inputfield', $this->className); $this->defaultInputfieldClass = str_replace('Fieldtype', 'Inputfield', $this->className);
if($this->className() === 'FieldtypeFile') $this->allowFieldtypes = $this->defaultAllowFieldtypes;
parent::__construct();
} }
/** /**
@@ -214,7 +243,11 @@ class FieldtypeFile extends FieldtypeMulti {
if(isset($v['modified'])) $pagefile->modified = $v['modified']; if(isset($v['modified'])) $pagefile->modified = $v['modified'];
if(isset($v['created'])) $pagefile->created = $v['created']; if(isset($v['created'])) $pagefile->created = $v['created'];
if(isset($v['tags'])) $pagefile->tags = $v['tags']; if(isset($v['tags'])) $pagefile->tags = $v['tags'];
if(!empty($v['filedata'])) $pagefile->filedata = json_decode($v['filedata'], true); if(!empty($v['filedata'])) {
$filedata = json_decode($v['filedata'], true);
unset($filedata['ix']);
$pagefile->filedata = $filedata;
}
$pagefile->setTrackChanges(true); $pagefile->setTrackChanges(true);
$pagefiles->add($pagefile); $pagefiles->add($pagefile);
} }
@@ -238,6 +271,8 @@ class FieldtypeFile extends FieldtypeMulti {
if(!$value instanceof Pagefiles) return $sleepValue; if(!$value instanceof Pagefiles) return $sleepValue;
foreach($value as $pagefile) { foreach($value as $pagefile) {
/** @var Pagefile $pagefile */
$item = array( $item = array(
'data' => $pagefile->basename, 'data' => $pagefile->basename,
'description' => $pagefile->description(true), 'description' => $pagefile->description(true),
@@ -255,9 +290,12 @@ class FieldtypeFile extends FieldtypeMulti {
} }
if($fileSchema & self::fileSchemaFiledata) { if($fileSchema & self::fileSchemaFiledata) {
$filedata = $this->sleepFiledata($field, $pagefile);
if(empty($filedata)) {
$item['filedata'] = null; $item['filedata'] = null;
$filedata = $pagefile->filedata; } else {
if(!empty($filedata)) $item['filedata'] = json_encode($filedata); $item['filedata'] = json_encode($filedata);
}
} }
$sleepValue[] = $item; $sleepValue[] = $item;
@@ -265,8 +303,121 @@ class FieldtypeFile extends FieldtypeMulti {
return $sleepValue; return $sleepValue;
} }
/**
* Get the filedata from given $pagefile ready for placement in a sleep value
*
* @param Field $fileField Field having type FieldtypeFile or FieldtypeImage (or derivative)
* @param Pagefile|Pageimage $pagefile
* @return array Sleep value array
* @since 3.0.142
*
*/
protected function sleepFiledata(Field $fileField, Pagefile $pagefile) {
$filedata = $pagefile->filedata;
$template = $this->getFieldsTemplate($fileField);
if(!$template) return $filedata; // custom field template not in use, return filedata as-is
if(!is_array($filedata)) $filedata = array();
$fieldValues = $pagefile->fieldValues; // field values that were loaded
$fieldgroup = $template->fieldgroup;
$idKeys = array(); // _123 type field ID keys that are populated
$mockPage = $this->getFieldsPage($fileField);
// sleep values from pagefile->fieldValues and place back into filedata
foreach($fieldValues as $key => $value) {
$field = $fieldgroup->getFieldContext($key);
if(!$field) continue;
$idKey = "_$field->id";
if($value === null) {
// null to remove value
unset($filedata[$idKey]);
continue;
}
$sleepValue = $field->type->sleepValue($mockPage, $field, $value);
if($sleepValue === null) {
unset($filedata[$idKey]);
} else {
$filedata[$idKey] = $sleepValue;
$idKeys[$idKey] = $idKey;
}
}
// check for data that should no longer be here
// validate that all field ID keys resolve to fields in the fieldgroup
foreach($filedata as $key => $value) {
if(isset($idKeys[$key])) continue; // valid, skip
if(strpos($key, '_') !== 0) continue; // some other filedata, skip
$fieldID = ltrim($key, '_');
if(!ctype_digit($fieldID)) continue; // not a custom field, skip
if($fieldgroup->hasField((int) $fieldID)) continue; // valid, skip
unset($filedata[$key]); // at this point, it can be removed
}
// build fieldValues index
$index = array();
foreach($filedata as $idKey => $value) {
$id = ltrim($idKey, '_');
if(!ctype_digit($id)) continue;
$key = "{$id}_";
$indexValue = $this->filedataIndexValue($key, $value);
if($indexValue !== null) $index[] = $indexValue;
}
unset($filedata['ix']);
if(count($index)) $filedata['ix'] = implode(' ', $index);
return $filedata;
}
/**
* Get indexable value for filedata or null if not one that will be indexed
*
* @param string $key
* @param mixed $value
* @return null|string
*
*/
protected function filedataIndexValue($key, $value) {
$indexValue = null;
if(is_array($value) && count($value) && !is_array(reset($value))) {
$a = array();
foreach($value as $k => $v) {
if(is_array($v) || !strlen("$v")) continue;
$kk = is_int($k) ? $key : "{$key}{$k}_";
$v = $this->filedataIndexValue($kk, $v);
if($v !== null) $a[] = $v;
}
if(count($a)) $indexValue = implode(' ', $a);
} else if(is_int($value)) {
// integer index
$indexValue = "$key$value";
} else if(is_string($value) && ctype_alnum($value) && strlen($value) <= 128) {
// one word string
$indexValue = "$key$value";
}
return $indexValue;
}
/**
* Export value
*
* @param Page $page
* @param Field $field
* @param array|float|int|null|object|string $value
* @param array $options
* @return array|float|int|string
*
*/
public function ___exportValue(Page $page, Field $field, $value, array $options = array()) { public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
/** @var Pagefiles $pagefiles */
$pagefiles = $value; $pagefiles = $value;
$value = $this->sleepValue($page, $field, $value); $value = $this->sleepValue($page, $field, $value);
$exportValue = array(); $exportValue = array();
@@ -500,9 +651,32 @@ class FieldtypeFile extends FieldtypeMulti {
return $out; return $out;
} }
/**
* Get match query
*
* @param DatabaseQuerySelect $query
* @param string $table
* @param string $subfield
* @param string $operator
* @param mixed $value
* @return DatabaseQuery|DatabaseQuerySelect
* @throws PageFinderSyntaxException
* @throws WireException
*
*/
public function getMatchQuery($query, $table, $subfield, $operator, $value) { public function getMatchQuery($query, $table, $subfield, $operator, $value) {
if(in_array($operator, array("*=", "~=", "%=", "^=", "$="))) { if($subfield && !in_array($subfield, array('data', 'count', 'filedata', 'created', 'modified', 'description', 'tags'))) {
// match custom fields
if($this->getMatchQuerySubfield($query, $subfield, $operator, $value)) {
// successfully handled
} else {
// requested subfield does not match what's available
throw new PageFinderSyntaxException("Property '$subfield' not recognized in selector: $query->selector");
}
}
if(in_array($operator, array("*=", "~=", "!~=", "%=", "^=", "$="))) {
// fulltext match filename or description // fulltext match filename or description
/** @var DatabaseQuerySelectFulltext $ft */ /** @var DatabaseQuerySelectFulltext $ft */
$ft = $this->wire(new DatabaseQuerySelectFulltext($query)); $ft = $this->wire(new DatabaseQuerySelectFulltext($query));
@@ -511,15 +685,117 @@ class FieldtypeFile extends FieldtypeMulti {
} else { } else {
$query = parent::getMatchQuery($query, $table, $subfield, $operator, $value); $query = parent::getMatchQuery($query, $table, $subfield, $operator, $value);
} }
return $query; return $query;
} }
/**
* Get match query for custom field selector
*
* @param DatabaseQuerySelect $query
* @param string $subfield
* @param string $operator
* @param mixed $value
* @return bool
* @throws PageFinderSyntaxException
*
*/
protected function getMatchQuerySubfield($query, &$subfield, &$operator, &$value) {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
$selector = $query->selector;
$property = '';
if($selector && substr_count($selector->field(), '.') > 1) {
// field in format field.subfield.property
$parts = explode('.', $selector->field());
while(count($parts) > 2) $property = array_pop($parts);
}
$field = $this->wire('fields')->get($subfield);
$fileField = $query->field;
if($fileField && !$fileField->type instanceof FieldtypeFile) $fileField = null;
if(!$field || !$fileField) return false;
$template = $this->getFieldsTemplate($fileField);
if(!$template || !$template->fieldgroup->hasField($field)) {
throw new PageFinderSyntaxException("Field '$fileField->name' does not have subfield '$field->name'");
}
if($field->type instanceof FieldtypePage) {
if($property) {
throw new PageFinderSyntaxException("Property '$property' not supported in field '" . $selector->field() . "'");
} else if(!ctype_digit("$value") && $sanitizer->pagePathName($value) === $value) {
// page path to ID
$p = $this->wire('pages')->get($value);
if($p->id && $p->viewable(false)) $value = $p->id;
}
}
if(($operator === '=' || $operator === '!=') && ctype_alnum("$value") && strlen("$value") <= 128) {
// we can match our index value in filedata[ix]
$operator = $operator === '=' ? '~=' : '!~=';
$value = $property ? "{$field->id}_{$property}_$value" : "{$field->id}_$value";;
} else if($operator === '=') {
$operator = ctype_alnum("$value") ? '~=' : '*='; // ok
} else if($operator === '!=' && ctype_alnum("$value")) {
$operator = '!~='; // ok
} else if($operator === '~=' || $operator === '!~=' || $operator === '%=' || $operator === '*=') {
// ok
} else {
throw new PageFinderSyntaxException("Operator $operator is not supported by $this in selector: $selector");
}
$subfield = 'filedata';
return true;
}
/**
* Get selector info
*
* @param Field $field
* @param array $data
* @return array
*
*/
public function ___getSelectorInfo(Field $field, array $data = array()) { public function ___getSelectorInfo(Field $field, array $data = array()) {
$info = parent::___getSelectorInfo($field, $data); $info = parent::___getSelectorInfo($field, $data);
$info['subfields']['data']['label'] = $this->_('filename'); $info['subfields']['data']['label'] = $this->_('filename');
$template = $this->getFieldsTemplate($field);
if($template) {
foreach($template->fieldgroup as $f) {
$f = $template->fieldgroup->getFieldContext($f);
if($f->type instanceof FieldtypePage) {
$info['subfields'][$f->name] = array(
'name' => $f->name,
'label' => $f->getLabel(),
'operators' => array('=', '!='),
'input' => 'page',
'options' => array(),
);
} else if($f->type instanceof FieldtypeCheckbox || $f->type->className() === 'FieldtypeToggle') {
$info['subfields'][$f->name] = $f->type->getSelectorInfo($f, $data);
} else {
}
}
}
return $info; return $info;
} }
/**
* Get database schema
*
* @param Field $field
* @return array
*
*/
public function getDatabaseSchema(Field $field) { public function getDatabaseSchema(Field $field) {
$database = $this->wire('database'); $database = $this->wire('database');
@@ -747,7 +1023,58 @@ class FieldtypeFile extends FieldtypeMulti {
return null; return null;
} }
/**
* Return Template used to manage fields for given file field
*
* #pw-internal
*
* @param Field $field Field having type FieldtypeFile or FieldtypeImage
* @return Template|null Returns Template or null if it does not exist
* @since 3.0.142
*
*/
public function getFieldsTemplate(Field $field) {
$template = $this->wire('templates')->get('field-' . $field->name);
if(!$template) return null;
// prepare fieldgroup used by template
$fieldgroup = $template->fieldgroup;
if($this->className() === 'FieldtypeFile') {
$allowFieldtypes = $this->allowFieldtypes;
} else {
$allowFieldtypes = $this->wire('fieldtypes')->get('FieldtypeFile')->get('allowFieldtypes');
}
$allowFieldtypes = array_flip($allowFieldtypes);
foreach($fieldgroup as $f) {
$name = str_replace('Fieldtype', '', $f->type->className());
if(!isset($allowFieldtypes[$name])) $fieldgroup->softRemove($f);
}
return $template;
}
/**
* Get a mock/placeholder page for using custom fields in files
*
* #pw-internal
*
* @param Field $field
* @return Page
* @since 3.0.142
*
*/
public function getFieldsPage(Field $field) {
$page = new Page();
$page->template = $this->getFieldsTemplate($field);
$page->status = Page::statusOn | Page::statusCorrupted; // corrupted status prevents saving
return $page;
}
/**
* Field config
*
* @param Field $field
* @return InputfieldWrapper
*
*/
public function ___getConfigInputfields(Field $field) { public function ___getConfigInputfields(Field $field) {
$inputfields = parent::___getConfigInputfields($field); $inputfields = parent::___getConfigInputfields($field);
@@ -891,6 +1218,83 @@ class FieldtypeFile extends FieldtypeMulti {
return $inputfields; return $inputfields;
} }
/**
* Module config
*
* @param InputfieldWrapper $inputfields
*
*/
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
if($this->className() != 'FieldtypeFile') return;
/** @var InputfieldCheckboxes $f */
$f = $this->wire('modules')->get('InputfieldCheckboxes');
$f->attr('name', 'allowFieldtypes');
$f->label = $this->_('Allowed Fieldtype modules for custom fields');
$f->description = $this->_('Types with strikethrough are not likely to be 100% compatible.');
$f->optionColumns = 3;
$f->entityEncodeText = false;
$moduleNames = $this->wire('modules')->findByPrefix('Fieldtype');
ksort($moduleNames);
$names = array();
// types always disallowed
$blacklist = array(
'File',
'Image',
'Repeater',
'RepeaterMatrix',
'PageTable',
'Options',
'Comments',
'Table'
);
foreach($moduleNames as $key => $moduleName) {
list(,$name) = explode('Fieldtype', $moduleName, 2);
$names[$name] = $name;
if(in_array($name, $blacklist)) {
unset($names[$name]);
continue;
}
if(in_array($name, $this->defaultAllowFieldtypes)) continue; // these are fine
// check schema of field by finding an example of one
$allow = false;
foreach($this->wire('fields') as $field) {
// @var Field $field
if($field->type instanceof FieldtypeFile) continue;
if(!wireInstanceOf($field->type, $moduleName)) continue;
// verify that field DB table is responsible for all data created by the field
$schema = $field->type->getDatabaseSchema($field);
if(isset($schema['xtra']['all']) && $schema['xtra']['all'] !== true) continue;
unset($schema['data'], $schema['pages_id'], $schema['keys'], $schema['xtra']);
// if there's not any other schema required by the Fieldtype, it can be supported here
if(!count($schema)) $allow = true;
break;
}
if(!$allow) {
// indicate with strikethrough potential issue with this type
$names[$name] = "<s>$name</s>";
}
}
foreach($names as $key => $name) {
$f->addOption($key, $name);
}
$f->val($this->allowFieldtypes);
$inputfields->add($f);
}
} }

File diff suppressed because one or more lines are too long

View File

@@ -338,7 +338,15 @@ $(document).ready(function() {
} }
} }
setTimeout(function() {
var $inputfields = $markup.find('.Inputfield');
if($inputfields.length) {
InputfieldsInit($markup.find('.Inputfields'));
$inputfields.trigger('reloaded', ['InputfieldFileUpload']);
} }
}, 500);
} // for
$progressItem.remove(); $progressItem.remove();

File diff suppressed because one or more lines are too long

View File

@@ -30,6 +30,7 @@
* @method void processInputAddFile($filename) * @method void processInputAddFile($filename)
* @method void processInputDeleteFile(Pagefile $pagefile) * @method void processInputDeleteFile(Pagefile $pagefile)
* @method bool processInputFile(WireInputData $input, Pagefile $pagefile, $n) * @method bool processInputFile(WireInputData $input, Pagefile $pagefile, $n)
* @method bool processItemInputfields(Pagefile $pagefile, InputfieldWrapper $inputfields, $id, WireInputData $input)
* *
*/ */
class InputfieldFile extends Inputfield implements InputfieldItemList, InputfieldHasSortableValue { class InputfieldFile extends Inputfield implements InputfieldItemList, InputfieldHasSortableValue {
@@ -38,7 +39,7 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
return array( return array(
'title' => __('Files', __FILE__), // Module Title 'title' => __('Files', __FILE__), // Module Title
'summary' => __('One or more file uploads (sortable)', __FILE__), // Module Summary 'summary' => __('One or more file uploads (sortable)', __FILE__), // Module Summary
'version' => 125, 'version' => 126,
'permanent' => true, 'permanent' => true,
); );
} }
@@ -109,6 +110,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
*/ */
protected $labels = array(); protected $labels = array();
/**
* Cached value of Fieldgroup used for Pagefile custom fields, as used by getItemInputfields() method
*
* @var Fieldgroup|null|bool Null when not yet known, false when known not applicable, Fieldgroup when known and in use
*
*/
protected $itemFieldgroup = null;
/** /**
* Initialize the InputfieldFile * Initialize the InputfieldFile
* *
@@ -477,16 +486,20 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
"<input type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />" . "<input type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />" .
"<i class='fa fa-fw fa-trash'></i></label>"; "<i class='fa fa-fw fa-trash'></i></label>";
$out .= $description = $this->renderItemDescriptionField($pagefile, $id, $n);
"</p>" . $class = 'InputfieldFileData ';
"<div class='InputfieldFileData description ui-widget-content'>" . $class .= $description ? 'description ui-widget-content' : 'InputfieldFileFields';
$this->renderItemDescriptionField($pagefile, $id, $n);
if(!$this->renderValueMode) $out .= $out .= "</p><div class='$class'>" . $description;
"<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />";
$out .= $inputfields = $this->getItemInputfields($pagefile);
"</div>"; if($inputfields) $out .= $inputfields->render();
if(!$this->renderValueMode) {
$out .= "<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />";
}
$out .= "</div>";
return $out; return $out;
} }
@@ -661,6 +674,8 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
} }
} }
$this->getItemInputfields(); // custom fields ready
return parent::renderReady($parent, $renderValueMode); return parent::renderReady($parent, $renderValueMode);
} }
@@ -748,6 +763,8 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
} }
/** /**
* Process input to add a file
*
* @param string $filename * @param string $filename
* @throws WireException * @throws WireException
* *
@@ -822,6 +839,12 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
} }
} }
/**
* Process input to delete a Pagefile item
*
* @param Pagefile $pagefile
*
*/
protected function ___processInputDeleteFile(Pagefile $pagefile) { protected function ___processInputDeleteFile(Pagefile $pagefile) {
$fileLabel = $this->wire('config')->debug ? $pagefile->url() : $pagefile->name; $fileLabel = $this->wire('config')->debug ? $pagefile->url() : $pagefile->name;
$this->message($this->_("Deleted file:") . " $fileLabel"); // Label that precedes a deleted filename $this->message($this->_("Deleted file:") . " $fileLabel"); // Label that precedes a deleted filename
@@ -829,8 +852,19 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
$this->trackChange('value'); $this->trackChange('value');
} }
/**
* Process input for one Pagefile
*
* @param WireInputData $input
* @param Pagefile $pagefile
* @param int $n
* @return bool
*
*/
protected function ___processInputFile(WireInputData $input, Pagefile $pagefile, $n) { protected function ___processInputFile(WireInputData $input, Pagefile $pagefile, $n) {
$saveFields = false; // allow custom Inputfields to be saved?
$changed = false; // are there any changes to this file?
$id = $this->name . '_' . $pagefile->hash; $id = $this->name . '_' . $pagefile->hash;
if($this->uploadOnlyMode) { if($this->uploadOnlyMode) {
@@ -839,8 +873,6 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
if($input->$key === null) return false; if($input->$key === null) return false;
} }
$changed = false;
// replace (currently only used by InputfieldImage) // replace (currently only used by InputfieldImage)
$key = "replace_$id"; $key = "replace_$id";
$replace = $input->$key; $replace = $input->$key;
@@ -918,9 +950,16 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
$sort = (int) $sort; $sort = (int) $sort;
$pagefile->sort = $sort; $pagefile->sort = $sort;
if($n !== $sort) $changed = true; if($n !== $sort) $changed = true;
$saveFields = true;
} }
} }
if($saveFields) {
// save custom Inputfields
$inputfields = $this->getItemInputfields($pagefile);
if($inputfields && $this->processItemInputfields($pagefile, $inputfields, $id, $input)) $changed = true;
}
if(isset($input['delete_' . $id])) { if(isset($input['delete_' . $id])) {
$this->processInputDeleteFile($pagefile); $this->processInputDeleteFile($pagefile);
$changed = true; $changed = true;
@@ -938,6 +977,48 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
return $changed; return $changed;
} }
/**
* Process custom Inputfields for Pagefile item
*
* @param Pagefile $pagefile
* @param InputfieldWrapper $inputfields
* @param string $id Pagefile ID string
* @param WireInputData $input
* @return bool True if changes detected, false if not
* @since 3.0.142
*
*/
protected function ___processItemInputfields(Pagefile $pagefile, InputfieldWrapper $inputfields, $id, WireInputData $input) {
$changed = false;
foreach($inputfields->getAll() as $f) {
$f->resetTrackChanges(true);
$f->processInput($input);
if(!$f->isChanged() && !$pagefile->isTemp()) {
continue;
}
$name = str_replace("_$id", '', $f->attr('name'));
if($f->getSetting('useLanguages')) {
$value = $pagefile->getFieldValue($name);
if(is_object($value)) $value->setFromInputfield($f);
} else {
$value = $f->val();
}
$pagefile->setFieldValue($name, $value, true);
$changed = true;
}
return $changed;
}
/**
* Process input
*
* @param WireInputData $input
* @return self
*
*/
public function ___processInput(WireInputData $input) { public function ___processInput(WireInputData $input) {
if(is_null($this->value)) $this->value = $this->wire(new Pagefiles($this->wire('page'))); if(is_null($this->value)) $this->value = $this->wire(new Pagefiles($this->wire('page')));
@@ -1080,7 +1161,13 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
* *
*/ */
protected function allowCollapsedItems() { protected function allowCollapsedItems() {
return $this->descriptionRows == 0 && !$this->useTags && !$this->noCollapseItem; $allow = $this->descriptionRows == 0 && !$this->useTags && !$this->noCollapseItem;
if($allow && $this->hasField) {
/** @var FieldtypeFile $fieldtype */
$fieldtype = $this->hasField->type;
if($fieldtype->getFieldsTemplate($this->hasField)) $allow = false;
}
return $allow;
} }
/** /**
@@ -1094,6 +1181,90 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
return $this->wire('sanitizer')->entities(str_replace(' ', ', ', trim($extensions))); return $this->wire('sanitizer')->entities(str_replace(' ', ', ', trim($extensions)));
} }
/**
* Get custom Inputfields for editing given Pagefile
*
* @param Pagefile|null $item Specify Pagefile item, or omit to prepare for render ready
* @return bool|InputfieldWrapper
* @since 3.0.142
*
*/
public function getItemInputfields(Pagefile $item = null) {
/** @var Pagefiles $pagefiles */
$value = $this->val();
$pagefiles = $value instanceof Pagefile ? $value->pagefiles : $value;
if(!$pagefiles instanceof Pagefiles) {
// no value present on this Inputfield
return false;
}
if($this->itemFieldgroup === false) {
// item fieldgroup already determined not in use
return false;
}
if($this->itemFieldgroup === null) {
// item fieldgroup not yet determined
$this->itemFieldgroup = false;
$template = $pagefiles->getFieldsTemplate();
if(!$template) return false;
$this->itemFieldgroup = $template->fieldgroup;
}
/** @var Page $page */
$page = $pagefiles->getFieldsPage();
$id = $item ? ('_' . $this->pagefileId($item)) : '';
$inputfields = $this->itemFieldgroup->getPageInputfields($page, $id, '', false);
if(!$inputfields) return false;
/** @var Languages|null $languages */
$languages = $this->wire('languages');
foreach($inputfields->getAll() as $f) {
if(!$item) {
// prepare inputfields for render rather than populating them
$f->renderReady();
continue;
}
/** @var Inputfield $f */
$name = str_replace($id, '', $f->name);
$value = $item ? $item->getFieldValue($name) : null;
if($value === null) continue;
if($languages && $f->getSetting('useLanguages') && $value instanceof LanguagesValueInterface) {
foreach($languages as $language) {
$f->set($language->isDefault() ? "value" : "value$language->id", $value->getLanguageValue($language->id));
}
} else {
$f->val($value);
}
if($f->className() === 'InputfieldCKEditor') {
// CKE does not like being placed in file/image fields.
// I'm sure it's possible, but needs more work and debugging, so it's disabled for now.
$allow = false;
} else {
$allow = true;
}
if(!$allow) {
$inputfields->remove($f);
$this->prependMarkup =
"<p class='ui-state-error-text'>" .
sprintf($this->_('Field “%1$s” type “%2$s” is not supported in field “%3$s”'), $f->label, $f->className(), $this->label) .
'</p>';
$f->getParent()->remove($f);
}
}
return $inputfields;
}
/** /**
* Configuration settings for InputfieldFile * Configuration settings for InputfieldFile
* *

View File

@@ -87,11 +87,17 @@ ul.InputfieldFileList li .ui-icon {
display: none !important; display: none !important;
} }
ul.InputfieldFileList li .InputfieldFileData { ul.InputfieldFileList li {
.InputfieldFileData {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
padding: 1em; padding: 1em;
} }
.InputfieldFileData.InputfieldFileFields {
padding: 0;
}
}
.InputfieldFile.InputfieldItemListCollapse ul.InputfieldFileList li .InputfieldFileData { .InputfieldFile.InputfieldItemListCollapse ul.InputfieldFileList li .InputfieldFileData {
display: none; display: none;

File diff suppressed because one or more lines are too long

View File

@@ -1330,10 +1330,10 @@ function InputfieldImage($) {
$grid.click(toggleClick); $grid.click(toggleClick);
if($target.hasClass('InputfieldImage')) { if($target.hasClass('InputfieldImage')) {
$target.find('.InputfieldHeader').append($list).append($left).append($grid); $target.children('.InputfieldHeader').append($list).append($left).append($grid);
defaultMode = getCookieData($target, 'mode'); defaultMode = getCookieData($target, 'mode');
} else { } else {
$(".InputfieldImage .InputfieldHeader", $target).append($list).append($left).append($grid); $(".InputfieldImage > .InputfieldHeader", $target).append($list).append($left).append($grid);
} }
if(defaultMode == 'list') { if(defaultMode == 'list') {
@@ -2041,6 +2041,13 @@ function InputfieldImage($) {
} else { } else {
setGridSize($inputfield, size); setGridSize($inputfield, size);
} }
setTimeout(function() {
var $inputfields = $markup.find('.Inputfield');
if($inputfields.length) {
InputfieldsInit($markup.find('.Inputfields'));
$inputfields.trigger('reloaded', ['InputfieldImageUpload']);
}
}, 250);
}).css("display", ""); }).css("display", "");
$markup.addClass('InputfieldFileItemExisting'); $markup.addClass('InputfieldFileItemExisting');

File diff suppressed because one or more lines are too long

View File

@@ -53,7 +53,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
return array( return array(
'title' => __('Images', __FILE__), // Module Title 'title' => __('Images', __FILE__), // Module Title
'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary 'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
'version' => 122, 'version' => 123,
'permanent' => true, 'permanent' => true,
); );
} }
@@ -672,11 +672,13 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
</div> </div>
"; ";
// @todo move the following to separate method shared by the renderSingle
$ext = $pagefile->ext(); $ext = $pagefile->ext();
$basename = $pagefile->basename(false); $basename = $pagefile->basename(false);
$focus = $pagefile->focus(); $focus = $pagefile->focus();
$inputfields = $this->getItemInputfields($pagefile);
if($inputfields) $additional .= $inputfields->render();
$out .= " $out .= "
<div class='ImageData'> <div class='ImageData'>
<h2 class='InputfieldImageEdit__name'><span contenteditable='true'>$basename</span>.$ext</h2> <h2 class='InputfieldImageEdit__name'><span contenteditable='true'>$basename</span>.$ext</h2>
@@ -1063,6 +1065,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$field->columnWidth = 50; $field->columnWidth = 50;
$fieldset->add($field); $fieldset->add($field);
/** @var InputfieldInteger $field */
$field = $this->modules->get('InputfieldInteger'); $field = $this->modules->get('InputfieldInteger');
$field->attr('name', 'clientQuality'); $field->attr('name', 'clientQuality');
$field->label = $this->_('Client-side resize quality percent for JPEGs'); $field->label = $this->_('Client-side resize quality percent for JPEGs');

View File

@@ -459,7 +459,7 @@ $focusPointCircleSize: 40px;
display: inline-block; display: inline-block;
display: flex; display: flex;
vertical-align: top; vertical-align: top;
align-items: center; // align-items: center;
justify-content: space-around; justify-content: space-around;
width: 300px; width: 300px;
border-right: 1px solid #eee; border-right: 1px solid #eee;