mirror of
https://github.com/processwire/processwire.git
synced 2025-08-08 15:57:01 +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:
@@ -12,7 +12,7 @@
|
||||
* Pagefile objects are contained by a `Pagefiles` object.
|
||||
* #pw-body
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @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 $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 $fieldValues Custom field values. #pw-internal @since 3.0.142
|
||||
* @property int $sort Sort order in database. #pw-group-other
|
||||
* @property string $basename Returns the filename without the path.
|
||||
* @property string $description Value of the file’s description field (string), if enabled. Note you can also set this property directly.
|
||||
@@ -77,6 +78,17 @@ class Pagefile extends WireData {
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -227,6 +239,8 @@ class Pagefile extends WireData {
|
||||
$language = $languages->get((int) $matches[1]);
|
||||
if($language && $language->id) return $this->setDescription($value, $language);
|
||||
}
|
||||
} else if($this->setFieldValue($key, $value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
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 file’s database record.
|
||||
*
|
||||
*
|
||||
* - 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 set a value, specify the $key and the $value to set.
|
||||
@@ -558,11 +571,120 @@ class Pagefile extends WireData {
|
||||
$value = filemtime($this->filename());
|
||||
if(strpos($key, 'Str')) $value = wireDate($this->wire('config')->dateFormat, $value);
|
||||
break;
|
||||
case 'fieldValues':
|
||||
return $this->fieldValues;
|
||||
break;
|
||||
default:
|
||||
$value = $this->getFieldValue($key);
|
||||
|
||||
}
|
||||
if(is_null($value)) return parent::get($key);
|
||||
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 there’s 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
|
||||
*
|
||||
|
@@ -109,6 +109,12 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
*/
|
||||
protected $formatted = false;
|
||||
|
||||
/**
|
||||
* @var Template|null
|
||||
*
|
||||
*/
|
||||
protected $fieldsTemplate = null;
|
||||
|
||||
/**
|
||||
* Construct a Pagefiles object
|
||||
*
|
||||
@@ -117,6 +123,7 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
*/
|
||||
public function __construct(Page $page) {
|
||||
$this->setPage($page);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -917,6 +924,44 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
|
||||
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
|
||||
*
|
||||
|
@@ -9,19 +9,20 @@
|
||||
* /wire/core/Fieldtype.php
|
||||
* /wire/core/FieldtypeMulti.php
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2017 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @property array $allowFieldtypes Allowed Fieldtype types for custom fields
|
||||
* @method string formatValueString(Page $page, Field $field, $value)
|
||||
*
|
||||
*/
|
||||
|
||||
class FieldtypeFile extends FieldtypeMulti {
|
||||
class FieldtypeFile extends FieldtypeMulti implements ConfigurableModule {
|
||||
|
||||
public static function getModuleInfo() {
|
||||
return array(
|
||||
'title' => __('Files', __FILE__),
|
||||
'version' => 105,
|
||||
'version' => 106,
|
||||
'summary' => __('Field that stores one or more files', __FILE__),
|
||||
'permanent' => true,
|
||||
);
|
||||
@@ -95,12 +96,40 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
*/
|
||||
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
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
$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['created'])) $pagefile->created = $v['created'];
|
||||
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);
|
||||
$pagefiles->add($pagefile);
|
||||
}
|
||||
@@ -238,6 +271,8 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
if(!$value instanceof Pagefiles) return $sleepValue;
|
||||
|
||||
foreach($value as $pagefile) {
|
||||
/** @var Pagefile $pagefile */
|
||||
|
||||
$item = array(
|
||||
'data' => $pagefile->basename,
|
||||
'description' => $pagefile->description(true),
|
||||
@@ -255,9 +290,12 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
}
|
||||
|
||||
if($fileSchema & self::fileSchemaFiledata) {
|
||||
$filedata = $this->sleepFiledata($field, $pagefile);
|
||||
if(empty($filedata)) {
|
||||
$item['filedata'] = null;
|
||||
$filedata = $pagefile->filedata;
|
||||
if(!empty($filedata)) $item['filedata'] = json_encode($filedata);
|
||||
} else {
|
||||
$item['filedata'] = json_encode($filedata);
|
||||
}
|
||||
}
|
||||
|
||||
$sleepValue[] = $item;
|
||||
@@ -265,8 +303,121 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
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()) {
|
||||
|
||||
/** @var Pagefiles $pagefiles */
|
||||
$pagefiles = $value;
|
||||
$value = $this->sleepValue($page, $field, $value);
|
||||
$exportValue = array();
|
||||
@@ -500,9 +651,32 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
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) {
|
||||
|
||||
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
|
||||
/** @var DatabaseQuerySelectFulltext $ft */
|
||||
$ft = $this->wire(new DatabaseQuerySelectFulltext($query));
|
||||
@@ -511,15 +685,117 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
} else {
|
||||
$query = parent::getMatchQuery($query, $table, $subfield, $operator, $value);
|
||||
}
|
||||
|
||||
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()) {
|
||||
$info = parent::___getSelectorInfo($field, $data);
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database schema
|
||||
*
|
||||
* @param Field $field
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function getDatabaseSchema(Field $field) {
|
||||
|
||||
$database = $this->wire('database');
|
||||
@@ -747,7 +1023,58 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
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) {
|
||||
|
||||
$inputfields = parent::___getConfigInputfields($field);
|
||||
@@ -891,6 +1218,83 @@ class FieldtypeFile extends FieldtypeMulti {
|
||||
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
@@ -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();
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -30,6 +30,7 @@
|
||||
* @method void processInputAddFile($filename)
|
||||
* @method void processInputDeleteFile(Pagefile $pagefile)
|
||||
* @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 {
|
||||
@@ -38,7 +39,7 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
return array(
|
||||
'title' => __('Files', __FILE__), // Module Title
|
||||
'summary' => __('One or more file uploads (sortable)', __FILE__), // Module Summary
|
||||
'version' => 125,
|
||||
'version' => 126,
|
||||
'permanent' => true,
|
||||
);
|
||||
}
|
||||
@@ -109,6 +110,14 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -477,16 +486,20 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
"<input type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />" .
|
||||
"<i class='fa fa-fw fa-trash'></i></label>";
|
||||
|
||||
$out .=
|
||||
"</p>" .
|
||||
"<div class='InputfieldFileData description ui-widget-content'>" .
|
||||
$this->renderItemDescriptionField($pagefile, $id, $n);
|
||||
$description = $this->renderItemDescriptionField($pagefile, $id, $n);
|
||||
$class = 'InputfieldFileData ';
|
||||
$class .= $description ? 'description ui-widget-content' : 'InputfieldFileFields';
|
||||
|
||||
if(!$this->renderValueMode) $out .=
|
||||
"<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />";
|
||||
$out .= "</p><div class='$class'>" . $description;
|
||||
|
||||
$out .=
|
||||
"</div>";
|
||||
$inputfields = $this->getItemInputfields($pagefile);
|
||||
if($inputfields) $out .= $inputfields->render();
|
||||
|
||||
if(!$this->renderValueMode) {
|
||||
$out .= "<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />";
|
||||
}
|
||||
|
||||
$out .= "</div>";
|
||||
|
||||
return $out;
|
||||
}
|
||||
@@ -661,6 +674,8 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
}
|
||||
}
|
||||
|
||||
$this->getItemInputfields(); // custom fields ready
|
||||
|
||||
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
|
||||
* @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) {
|
||||
$fileLabel = $this->wire('config')->debug ? $pagefile->url() : $pagefile->name;
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process input for one Pagefile
|
||||
*
|
||||
* @param WireInputData $input
|
||||
* @param Pagefile $pagefile
|
||||
* @param int $n
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
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;
|
||||
|
||||
if($this->uploadOnlyMode) {
|
||||
@@ -839,8 +873,6 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
if($input->$key === null) return false;
|
||||
}
|
||||
|
||||
$changed = false;
|
||||
|
||||
// replace (currently only used by InputfieldImage)
|
||||
$key = "replace_$id";
|
||||
$replace = $input->$key;
|
||||
@@ -918,9 +950,16 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
$sort = (int) $sort;
|
||||
$pagefile->sort = $sort;
|
||||
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])) {
|
||||
$this->processInputDeleteFile($pagefile);
|
||||
$changed = true;
|
||||
@@ -938,6 +977,48 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel
|
||||
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) {
|
||||
|
||||
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() {
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@@ -87,11 +87,17 @@ ul.InputfieldFileList li .ui-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
ul.InputfieldFileList li .InputfieldFileData {
|
||||
ul.InputfieldFileList li {
|
||||
.InputfieldFileData {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
.InputfieldFileData.InputfieldFileFields {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.InputfieldFile.InputfieldItemListCollapse ul.InputfieldFileList li .InputfieldFileData {
|
||||
display: none;
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1330,10 +1330,10 @@ function InputfieldImage($) {
|
||||
$grid.click(toggleClick);
|
||||
|
||||
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');
|
||||
} else {
|
||||
$(".InputfieldImage .InputfieldHeader", $target).append($list).append($left).append($grid);
|
||||
$(".InputfieldImage > .InputfieldHeader", $target).append($list).append($left).append($grid);
|
||||
}
|
||||
|
||||
if(defaultMode == 'list') {
|
||||
@@ -2041,6 +2041,13 @@ function InputfieldImage($) {
|
||||
} else {
|
||||
setGridSize($inputfield, size);
|
||||
}
|
||||
setTimeout(function() {
|
||||
var $inputfields = $markup.find('.Inputfield');
|
||||
if($inputfields.length) {
|
||||
InputfieldsInit($markup.find('.Inputfields'));
|
||||
$inputfields.trigger('reloaded', ['InputfieldImageUpload']);
|
||||
}
|
||||
}, 250);
|
||||
}).css("display", "");
|
||||
$markup.addClass('InputfieldFileItemExisting');
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -53,7 +53,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
return array(
|
||||
'title' => __('Images', __FILE__), // Module Title
|
||||
'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
|
||||
'version' => 122,
|
||||
'version' => 123,
|
||||
'permanent' => true,
|
||||
);
|
||||
}
|
||||
@@ -672,11 +672,13 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
|
||||
</div>
|
||||
";
|
||||
|
||||
// @todo move the following to separate method shared by the renderSingle
|
||||
$ext = $pagefile->ext();
|
||||
$basename = $pagefile->basename(false);
|
||||
$focus = $pagefile->focus();
|
||||
|
||||
$inputfields = $this->getItemInputfields($pagefile);
|
||||
if($inputfields) $additional .= $inputfields->render();
|
||||
|
||||
$out .= "
|
||||
<div class='ImageData'>
|
||||
<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;
|
||||
$fieldset->add($field);
|
||||
|
||||
/** @var InputfieldInteger $field */
|
||||
$field = $this->modules->get('InputfieldInteger');
|
||||
$field->attr('name', 'clientQuality');
|
||||
$field->label = $this->_('Client-side resize quality percent for JPEGs');
|
||||
|
@@ -459,7 +459,7 @@ $focusPointCircleSize: 40px;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
vertical-align: top;
|
||||
align-items: center;
|
||||
// align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 300px;
|
||||
border-right: 1px solid #eee;
|
||||
|
Reference in New Issue
Block a user