praiadeseselle/wire/modules/Inputfield/InputfieldSelector/InputfieldSelector.module

2190 lines
74 KiB
Text
Raw Permalink Normal View History

2022-03-08 15:55:41 +01:00
<?php namespace ProcessWire;
/**
* ProcessWire Selector Inputfield
*
* Concept by Avoine
* Code by Ryan Cramer
* Sponsored by Avoine
*
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com
*
* @todo add support for "custom: OR-group" option (https://processwire.com/talk/topic/13116-or-selecters-for-different-fields/)
* @todo support changes to a "title|body%=about" selector, so that a change to "title" row also updates the value for the "body" row.
* @todo update to support "sort=sort" as "sort" is not currently a selectable option
*
* SETTINGS
* ========
* Below are settings that may be passed to InputfieldSelector to customize the output and behavior.
* Each of the settings below is shown with the default value in [brackets]. True and false (boolean)
* values may be specified by 0 (false) or 1 (true).
*
* @property string $value [selector string] Value that user can change.
* @property string $initValue [selector string] Initial value that user can't change.
* @property Template|null $initTemplate [null] Optional context template, used only just 1 allowed (pulled from $initValue automatically).
* @property string $addIcon [plus-circle] Icon used for the "Add Field" link.
* @property string $addLabel [Add Field] Text used for the "Add Field" link
* @property string $dateFormat [Y-m-d] Default PHP date() format for date fields.
* @property string $datePlaceholder [yyyy-mm-dd] Placeholder attribute text for date fields.
* @property string $timeFormat [H:i] Default PHP date() format for time component of date fields.
* @property string $timePlaceholder [hh:mm] Placeholder attribute time component of date fields.
* @property string $exclude One or more (CSV) fields to disallow selection for
* @property string $optgroupsOrder [system,field,subfield,group,modifier,adjust] Order and presence of field selection option groups.
* @property string|array $previewColumns [name,template,id] Array or CSV string of columns to show in bookmark previews
* @property bool $preview [true] Whether to show a live preview of selector in notes section.
* @property bool $counter [true] Whether to show a live "number of matches" indicator.
* @property bool $allowAddRemove [true] Whether to allow adding new rows / removing existing rows
* @property bool $allowSystemCustomFields [false] Allow system custom fields to appear in field selects?
* @property bool $allowSystemNativeFields [true] Allow system native fields to appear in field selects?
* @property bool $allowSystemTemplates [false] Allow selection of system templates (user, role, etc.)?
* @property bool $allowSubselectors [true] Allow use of subselectors?
* @property bool $allowSubfields [true] Allow use of subfields?
* @property bool $allowSubfieldGroups [true] Allow @grouping of subfields (like for page references)?
* @property bool $allowModifiers [true] Allow use of modifiers like include, limit?
* @property bool $allowBlankValues [false] When no value is present, should it contribute to the selector?
* @property bool|int $showFieldLabels [false] Show field labels rather than names? Or specify integer 2 to show both.
* @property bool $showOptgroups [true] Whether or not to separate sytem, field, subfields, etc. into their own optgroups. If turned off, all types of fields will be shown together A-Z.
* @property array $limitFields [empty] Selectable fields whitelist (field names). Leave empty to allow any.
* @property bool $parseVars [true] Whether variables in a selector should be parsed and converted to values
* @property string $subfieldIdentifier ...
* @property string $groupIdentifier (1)
* @property int $maxUsers [20] Maximum number of users selectable. If more than regular text input used rather than select.
* @property string $selectClass
* @property string $inputClass
* @property string $checkboxClass
* @property string $lastSelector
* @property int $maxSelectOptions If quantity of select options exceeds this number, use autocomplete instead for Page reference fields (since 3.0.148)
*
*
*/
class InputfieldSelector extends Inputfield implements ConfigurableModule {
public static function getModuleInfo() {
return array(
'title' => 'Selector',
'version' => 28,
'summary' => 'Build a page finding selector visually.',
'author' => 'Avoine + ProcessWire',
'autoload' => "template=admin",
'addFlag' => Modules::flagsNoUserConfig
);
}
const debug = false;
/**
* Contains all possible operators, indexed by operator with values as labels describing the operator
*
* @var array
*/
protected $operators = array();
/**
* Array indexed by input types to operator arrays where values are the operators (no labels)
*
* @var array
*
*/
protected $operatorsByType = array();
/**
* Characters that should be automatically trimmed from any operators
*
* @var string
*
*/
protected $operatorTrimChars = '';
/**
* Array of predefined system fields indexed by field name and Fieldtype::getSelectorInfo style arrays as the value
*
* @var array
*
*/
protected $systemFields = array();
/**
* Same as $systemFields except contains only system fields applicable to page references
*
* @var array
*
*/
protected $systemPageFields = array();
/**
* Fields that modify behaviors of selectors but don't refer to any data: sort, limit, include
*
* @var array
*
*/
protected $modifierFields = array();
/**
* Instance of Selectors, when in the process of rendering
*
* @var Selectors
*
*/
protected $selectors = null;
/**
* If initValue has a SINGLE template defined, it will be present here
*
* @var Template|null
*
*/
protected $initTemplate = null;
/**
* Has the ready method been called?
* @var bool
*
*/
protected $isReady = false;
/**
* Variable names that should be picked up through the session each time
*
*/
protected $sessionVarNames = array(
'allowSystemCustomFields',
'allowSystemNativeFields',
'allowSystemTemplates',
'allowSubselectors',
'allowSubfields',
'allowSubfieldGroups',
'allowModifiers',
'allowAddRemove',
'showFieldLabels',
'fieldsWhitelist',
'dateFormat',
'datePlaceholder',
'timeFormat',
'timePlaceholder',
'previewColumns',
'limitFields',
'maxUsers',
);
/**
* Default values of each setting, for outside configuration
*
* @var array
*
*/
protected $defaultSettings = array();
/**
* Initialize the default values used in Selector
*
* See the comments a the top of this file for descriptions of all settings initialized here.
*
*/
public function __construct() {
$this->attr('name', 'selector'); // default name, which presumably will be changed
$this->setting('preview', 1); // whether to show a live preview in notes section
$this->setting('previewColumns', array()); // column names to use for bookmarked previews of selector results (in Lister)
$this->setting('counter', 1); // whether to show the live ajax number-of-page-matches counter
$this->setting('initValue', ''); // initial selector value: not changeable by user
$this->setting('addIcon', 'plus-circle');
$this->setting('addLabel', $this->_('Add Field'));
$this->setting('parseVars', true); // whether variables like [user.id] will be parsed
$this->set('lastSelector', ''); // last created selector
$this->set('subfieldIdentifier', ' &hellip;');
$this->set('groupIdentifier', ' ' . $this->_('(1)')); // group identifier
// what is allowed
$this->setting('allowAddRemove', true);
$this->setting('allowSystemCustomFields', false);
$this->setting('allowSystemNativeFields', true);
$this->setting('allowSystemTemplates', false);
$this->setting('allowSubselectors', true);
$this->setting('allowSubfields', true);
$this->setting('allowSubfieldGroups', true);
$this->setting('allowModifiers', true);
$this->setting('allowBlankValues', false); // blank values are allowed in selector or if not present, blank values allowed and represented by =""
$this->setting('showFieldLabels', false); // makes it use field labels, rather than names (when true)
$this->setting('showOptgroups', true); // show option groups to separate system, field, subfield, etc.
$this->setting('optgroupsOrder', 'system,field,subfield,group,modifier,adjust'); // field selection option groups and order to render them in (applicable only if showOptgroups=true)
$this->setting('exclude', ''); // CSV fields to disallow
$this->setting('limitFields', array()); // only allow these fields (empty=allow any)
$this->setting('dateFormat', $this->_('Y-m-d')); // date format
$this->setting('datePlaceholder', $this->_('yyyy-mm-dd')); // date format placeholder (what users see)
$this->setting('timeFormat', $this->_('H:i')); // time format
$this->setting('timePlaceholder', $this->_('hh:mm')); // time format placeholder (what users see)
$this->setting('maxUsers', 20); // text input rather than select used when useres qty is greater than this
$this->setting('maxSelectOptions', 100); // if quantity of pages meets or exceeds this number, use autocomplete for Page reference fields
parent::__construct();
}
/**
* Same as set() except that this remembers the default value populated to it for later retrieval from getDefaultSettings()
*
* @param $key
* @param $value
*
*/
protected function setting($key, $value) {
$this->set($key, $value);
$this->defaultSettings[$key] = $value;
}
/**
* Return the default settings
*
* @return array of key=>value
*
*/
public function getDefaultSettings() {
return $this->defaultSettings;
}
/**
* Return the configured settings
*
* @return array of key=>value
*
*/
public function getSettings() {
$settings = $this->defaultSettings;
foreach($this->getArray() as $key => $value) {
if(array_key_exists($key, $settings)) $settings[$key] = $value;
}
return $settings;
}
/**
* Monitor and respond to AJAX events
*
*/
public function ready() {
if($this->isReady) return;
$this->isReady = true;
// settings specified in $config->InputfieldSelector
$configDefaults = array(
'selectClass' => '',
'inputClass' => '',
'checkboxClass' => '',
);
$configSettings = $this->wire('config')->InputfieldSelector;
$configSettings = is_array($configSettings) ? array_merge($configDefaults, $configSettings) : $configDefaults;
foreach($configSettings as $key => $value) {
$this->setting($key, $value);
}
$input = $this->wire('input');
$name = $input->get('name');
$action = $input->get($this->className());
if(!$action || !$name) return;
if(!self::debug && !$this->wire('config')->ajax) return;
$this->attr('name', $this->wire('sanitizer')->fieldName($name)); // for session validity
if(!$this->sessionGet('valid')) return;
if(!$this->wire('user')->isLoggedin()) return;
$this->set('initValue', $this->sessionGet('initValue'));
$sanitizer = $this->wire('sanitizer');
foreach($this->sessionVarNames as $key) {
$this->set($key, $this->sessionGet($key));
}
$this->setup();
$fieldName = $input->get('field');
if($fieldName !== null) $fieldName = $sanitizer->name($fieldName);
$type = $input->get('type');
if($type !== null) $type = $sanitizer->name($type);
$q = $input->get('q');
if($q !== null) $q = $sanitizer->text($q);
if($action === 'field') {
$out = $this->renderSelectField();
} else if($action === 'subfield' && $fieldName) {
$out = $this->renderSelectSubfield($fieldName);
} else if($action === 'opval' && $fieldName) {
$out = $this->renderOpval($fieldName, $type);
} else if($action === 'subfield-opval') {
$out = $this->renderSelectSubfield($fieldName) . '<split>' . $this->renderOpval($fieldName, $type);
} else if($action === 'test' && ($selector = $input->post('selector'))) {
$out = $this->renderTestSelector($selector);
} else if($action === 'autocomplete' && $fieldName && strlen($q)) {
$out = $this->renderAutocompleteJSON($fieldName, $q);
} else {
$out = "Ajax request missing required info";
}
echo $this->prep($out);
exit;
}
/**
* Prepare output for send
*
* @param $out
* @return string
*
*/
protected function prep($out) {
// ensures id attributes in Inputfields (like dates) don't get array-like names
$out = preg_replace('/ id=[\'"]Inputfield_[a-zA-Z0-9_]+__value\[\][\'"]/', ' ', $out);
return $out;
}
/**
* Setup the shared structures and data used by Selector
*
*/
public function setup() {
$notLabel = $this->_('Not: %s');
$findOperators = array();
$findNotOperators = array();
$operators = Selectors::getOperators(array(
'getIndexType' => 'operator',
'getValueType' => 'verbose',
));
foreach($operators as $operator => $info) {
$this->operators[$operator] = $info['label'];
if($info['compareType'] & Selector::compareTypeFind) {
$findOperators[$operator] = $info['label'];
$findNotOperators["!$operator"] = sprintf($notLabel, $info['label']);
}
}
$this->operators = array_merge($this->operators, $findNotOperators, array(
'.=' => $this->_('Ascending By'),
'.=-' => $this->_('Descending By'),
'@=' => $this->_('Has'),
'@!=' => $this->_('Does Not Have'),
'=""' => $this->_('Is Empty'),
'!=""' => $this->_('Is Not Empty'),
//'#=' => $this->_('Matches'),
//'#!=' => $this->_('Does Not Match'),
));
// operators by input type
// this is a backup and/or for system fields, as these may also be specified
// with fieldtype's getSelectorInfo() method, which takes precedence
$this->operatorsByType = array(
'name' => array('=', '!=', '%='),
// 'text' => array('%=', '!%=', '*=', '!*=', '~=', '!~=', '^=', '!^=', '$=', '!$=', '=', '!=', '=""', '!=""'),
'text' => array_merge($findOperators, array('=', '!=', '<', '<=', '>', '>='), $findNotOperators),
'autocomplete' => array('=', '!='),
'number' => array('=', '!=', '<', '>', '<=', '>=', '=""', '!=""'),
'datetime' => array('=', '!=', '<', '>', '<=', '>='),
'page' => array('@=', '@!='),
'checkbox' => array('=', '!='),
'sort' => array('.=', '.=-'),
'status' => array('@=', '@!=', '<', '<=', '>', '>='),
//'selector' => array('#=', '#!='),
'selector' => array('=', '!=', '<', '>', '<=', '>='),
);
// chars that are trimmed off operators before being used
// enables different contexts for the same operator
$this->operatorTrimChars = '.@';
$templates = array();
$initTemplates = $this->getTemplatesFromInitValue($this->initValue);
foreach($this->wire()->templates as $template) {
if(($template->flags & Template::flagSystem) && !$this->allowSystemTemplates) {
continue;
}
if(count($initTemplates) && !in_array($template->id, $initTemplates)) continue;
$label = $template->getLabel();
if($label != $template->name) $label .= " ($template->name)";
$templates[$template->id] = $label;
}
// make users selectable if there are under $maxUsers of them
// otherwise utilize the user ID property
$users = array();
$userTemplates = implode('|', $this->wire('config')->userTemplateIDs);
$numUsers = $this->wire('pages')->count("template=$userTemplates, include=all");
if($numUsers < $this->maxUsers) {
$usersInput = 'select';
$usersOperators = array('=', '!=');
foreach($this->wire('users') as $user) {
$users[$user->id] = $user->name;
}
} else {
$usersInput = 'name';
$usersOperators = array('=', '%=', '!=');
}
$titleField = $this->wire('fields')->get('title');
// system fields definitions
$this->systemFields = array(
'count' => array(
'input' => 'number',
'label' => $this->_('Count'),
'sanitizer' => 'integer',
),
'created' => array(
'input' => 'datetime',
'label' => $this->_('Created date'),
'operators' => $this->operatorsByType['datetime'],
),
'created_users_id' => array(
'input' => $usersInput,
'label' => $this->_('Created by user'),
'options' => $users,
'operators' => $usersOperators
),
'_custom' => array(
'input' => 'text',
'label' => $this->_('Custom (field=value)'),
'operators' => array(),
'placeholder' => $this->_('field=value'),
),
'has_parent' => array(
'input' => 'text',
'label' => $this->_('Has parent/ancestor'),
'operators' => array('=', '!='),
),
'id' => array(
'input' => 'number',
'label' => $this->_('ID'),
'sanitizer' => 'integer',
),
'modified' => array(
'input' => 'datetime',
'label' => $this->_('Modified date'),
'operators' => $this->operatorsByType['datetime'],
),
'modified_users_id' => array(
'input' => $usersInput,
'label' => $this->_('Modified by user'),
'options' => $users,
'operators' => $usersOperators
),
'name' => array(
'input' => 'text',
'label' => $this->_('Name'),
'sanitizer' => 'pageNameUTF8',
'operators' => array('=', '!=', '%='),
),
'num_children' => array(
'input' => 'number',
'label' => $this->_('Number of children'),
'sanitizer' => 'integer',
),
'parent' => array(
'input' => 'number', // select
'label' => $this->_x('Parent', 'parent-only'),
'operators' => array('=', '!='), // $this->operatorsByType['page']
// 'options' => array(id => label)
),
'parent.' => array(
'input' => 'subfields',
'label' => $this->_x('Parent',' parent-with-subfield'),
),
'path' => array(
'input' => 'text',
'label' => $this->_('Path/URL'),
'operators' => $this->operatorsByType['text'],
),
'published' => array(
'input' => 'datetime',
'label' => $this->_('Published date'),
'operators' => $this->operatorsByType['datetime'],
),
'status' => array(
'input' => 'select',
'label' => $this->_('Status'),
'options' => array(
'hidden' => $this->_('Hidden'),
'unpublished' => $this->_('Unpublished'),
'locked' => $this->_('Locked'),
'trash' => $this->_('Trash'),
'temp' => $this->_('Temp'),
),
'sanitizer' => 'integer',
'operators' => $this->operatorsByType['status'],
),
'template' => array(
'input' => 'select',
'label' => $this->_('Template'),
'options' => $templates,
'sanitizer' => 'integer',
'operators' => array('=', '!='),
),
'title' => array(
'input' => 'text',
'label' => ($titleField ? $titleField->getLabel() : 'Title'),
'operators' => $this->operatorsByType['text']
),
//'parent' => $this->_('parent'),
);
$ignoreStatuses = array(
Page::statusOn,
Page::statusReserved,
Page::statusSystem,
Page::statusSystemID,
);
foreach(Page::getStatuses() as $name => $status) {
if($status > Page::statusTrash) continue;
if($status === Page::statusDraft && !$this->wire('modules')->isInstalled('ProDrafts')) continue;
if(in_array($status, $ignoreStatuses)) continue;
if(isset($this->systemFields['status']['options'][$name])) continue; // use existing label
$this->systemFields['status']['options'][$name] = ucfirst($name);
}
if(!count($users)) {
unset($this->systemFields['modified_users_id']['options']);
unset($this->systemFields['created_users_id']['options']);
}
// system fields for page references
$this->systemPageFields = array(
'created' => $this->systemFields['created'],
'id' => $this->systemFields['id'],
'modified' => $this->systemFields['modified'],
'name' => $this->systemFields['name'],
'published' => $this->systemFields['published'],
'status' => $this->systemFields['status'],
);
$this->modifierFields = array(
'include' => array(
'input' => 'select',
'label' => $this->_('Include'),
'options' => array(
'hidden' => $this->_('Hidden'),
'unpublished' => $this->_('Hidden + Unpublished'),
'trash' => $this->_('Hidden + Unpublished + Trash'),
'all' => $this->_('All'),
),
'operators' => array('=')
),
'limit' => array(
'input' => 'integer',
'label' => $this->_('Limit'),
'operators' => array('=')
),
'sort' => array(
'input' => 'select',
'label' => $this->_('Sort'),
'sanitizer' => 'fieldName',
'operators' => array('.=', '.=-'),
'options' => array() // populated below
),
);
// populate the sort options
$options = array();
foreach($this->systemFields as $name => $f) {
$options[$name] = $f['label'];
}
foreach($this->wire('fields') as $f) {
if(strpos($f->type, 'FieldtypeFieldset') === 0) continue;
$options[$f->name] = $f->name;
}
ksort($options);
$this->modifierFields['sort']['options'] = $options;
}
/**
* Set a session variable specific to this Inputfield instance
*
* @param string $key
* @param mixed $value
* @return $this
*
*/
protected function sessionSet($key, $value) {
$s = $this->wire('session')->get($this->className());
if(!is_array($s)) $s = array();
if(count($s) > 100) $s = array_slice($s, -100); // prevent from growing too large
$id = 'id' . $this->wire('page')->id . "_" . $this->wire('sanitizer')->fieldName($this->attr('name'));
if(!isset($s[$id])) $s[$id] = array();
$s[$id][$key] = $value;
$this->wire('session')->set($this->className(), $s);
return $this;
}
/**
* Retrieve a session variable specific to this Inputfield instance
*
* @param string $key
* @return mixed
*
*/
protected function sessionGet($key) {
$s = $this->wire('session')->get($this->className());
if(!$s) return null;
$id = 'id' . $this->wire('page')->id . "_" . $this->wire('sanitizer')->fieldName($this->attr('name'));
if(empty($s[$id])) return null;
if(empty($s[$id][$key])) return null;
return $s[$id][$key];
}
/**
* Returns an array of selector information for the given Field or field name
*
* Front-end to Fieldtype::getSelectorInfo
*
* @param string|Field $field
* @return array Blank array if information not available
*
*/
public function getSelectorInfo($field) {
if(is_string($field)) {
if(isset($this->systemFields[$field])) {
return $this->systemFields[$field];
}
if(isset($this->modifierFields[$field])) {
return $this->modifierFields[$field];
}
$field = $this->wire('fields')->get($field);
}
if(!$field || !$field instanceof Field || !$field->type) return array();
$info = $field->type->getSelectorInfo($field);
if($info['input'] == 'page') {
$info['subfields'] = array_merge($info['subfields'], $this->systemPageFields);
}
if(!empty($info['subfields'])) {
$subfields = array();
foreach($info['subfields'] as $name => $subfield) {
// consider multi-language
if(strpos($name, 'data') === 0 && $this->wire('languages')) {
list($unused, $languageID) = explode('data', "x$name");
if($unused) {}
if(ctype_digit($languageID)) {
$language = $this->wire('languages')->get((int) $languageID);
if($language && $language->id) {
$subfield['label'] = $field->getLabel() . " (" . $language->get('title|name') . ")";
}
}
}
if(isset($this->systemFields[$name])) {
$label = isset($this->systemFields[$name]['label']) ? $this->systemFields[$name]['label'] : $name;
} else if(!empty($subfield['label'])) {
$label = $subfield['label'];
} else if(strpos($name, 'data') === 0 && ctype_digit(substr($name, 4)) && $this->wire('languages')) {
$label = $this->wire('languages')->get((int) substr($name, 4))->get('title|name');
} else {
$f = $this->wire('fields')->get($name);
$label = $f ? $f->getLabel() : $name;
}
$subfield['label'] = $label;
$key = $this->showFieldLabels ? "$label\t$name" : $name;
while(isset($subfields[$key])) $key .= " ";
$subfields[$key] = $subfield;
}
ksort($subfields);
if($this->showFieldLabels) {
// convert back to name-based keys
$_subfields = array();
foreach($subfields as $key => $subfield) {
list($label, $name) = explode("\t", $key);
if($label) {}
$_subfields[$name] = $subfield;
}
$subfields = $_subfields;
}
$info['subfields'] = $subfields;
}
return $info;
}
/**
* Does the given $field have subfields?
*
* @param Field $field
* @return bool
* @deprecated not currently in use
*
*/
protected function hasSubfields(Field $field) {
$info = $this->getSelectorInfo($field);
return count($info['subfields']) > 0;
}
/**
* Render the results of a selector test: how many pages match
*
* @param $selector
* @return string
*
*/
protected function renderTestSelector($selector) {
try {
$selector = $this->sanitizeSelectorString($selector);
$cnt = $this->wire('pages')->count($selector, array('allowCustom' => true));
$out = '';
// take into account a limit=n
if(strpos($selector, 'limit=') !== false && preg_match('/\blimit=(\d+)/', $selector, $matches)) {
$out = ' ' . sprintf($this->_('(%d without limit)'), $cnt);
if($cnt > $matches[1]) $cnt = $matches[1];
}
$out = sprintf($this->_n('matches %d page', 'matches %d pages', $cnt), $cnt) . $out;
if(self::debug) $out .= " (" . $selector . ")";
if($cnt > 0) {
// bookmarks for Lister
$bookmark = array(
'defaultSelector' => $selector,
'initSelector' => $this->initValue,
);
$previewColumns = $this->sessionGet('previewColumns');
if($previewColumns && is_string($previewColumns)) $previewColumns = explode(',', $previewColumns);
if(is_array($previewColumns) && count($previewColumns)) $bookmark['columns'] = $previewColumns;
$this->wire('modules')->includeModule('ProcessPageLister');
$id = ((int) $this->wire('input')->get('id')) . '_' . $this->attr('name');
$url = ProcessPageLister::addSessionBookmark($id, $bookmark);
if($url) {
$title = $this->_('Pages that match your selector');
$out .= " (<a class='pw-modal' title='$title' href='$url&minimal=1'>" . $this->_('show') . "</a>)";
}
}
} catch(\Exception $e) {
$out = $e->getMessage();
}
return $out;
}
/**
* Render the primary field <select>
*
* @param array $settings See defaults list in method
* @param string $selectedValue
* @return string
*
*/
protected function renderSelectField($settings = array(), $selectedValue = '') {
$defaults = array(
'name' => 'field',
'class' => 'select-field',
'showModifiers' => $this->allowModifiers,
'showSystem' => $this->allowSystemNativeFields,
'showSubfields' => $this->allowSubfields,
'showSubfieldGroups' => $this->allowSubfieldGroups,
'showFieldLabels' => $this->showFieldLabels,
'showOptgroups' => $this->showOptgroups,
'customFields' => $this->wire('fields'),
'exclude' => array('count', 'pass'),
'limitFields' => array(),
'templates' => array(),
'type' => '',
'prepend' => '<option></option>',
'append' => '',
);
$settings = array_merge($defaults, $settings);
if($this->exclude) {
$exclude = is_array($this->exclude) ? $this->exclude : explode(',', $this->exclude);
foreach($exclude as $k => $v) $exclude[$k] = trim($v);
$settings['exclude'] = array_merge($settings['exclude'], $exclude);
}
$limitSubfields = null; // becomes $limitField indexed array of subfield names when applicable
if(wireCount($this->limitFields)) {
$limitSubfields = array();
foreach($this->limitFields as $limitField) {
$settings['limitFields'][$limitField] = $limitField;
if(strpos($limitField, '.')) {
list($limitField, $limitSubfield) = explode('.', $limitField);
$settings['limitFields'][$limitField] = $limitField; // version without subfield
$limitSubfields[$limitField][$limitSubfield] = $limitSubfield;
} else {
if(!isset($limitSubfields[$limitField])) {
$limitSubfields[$limitField] = array();
}
}
}
}
$selectedValueSubfield = '';
if(strpos($selectedValue, '.') !== false && !isset($this->systemFields[$selectedValue])) {
list($selectedValue, $selectedValueSubfield) = explode('.', $selectedValue);
}
if($selectedValueSubfield) $selectedValue .= ".";
$outAll = array();
$outSections = array(
'system' => '',
'field' => '',
'subfield' => '',
'group' => '',
'modifier' => '',
'adjust' => '',
);
$outLabels = array(
'system' => $this->_x('System fields', 'optgroup-label'),
'field' => $this->_x('Fields', 'optgroup-label'),
'subfield' => $this->_x('Subfields', 'optgroup-label'),
'group' => $this->_x('Fields matching same (1) item', 'optgroup-label'),
'modifier' => $this->_x('Modifiers', 'optgroup-label'),
'adjust' => $this->_x('Adjustments', 'optgroup-label'),
);
$optgroups = array();
if($settings['showSystem']) {
$optgroups['system'] = $this->systemFields;
if($this->initTemplate) {
$nameLabel = $this->initTemplate->getNameLabel();
if($nameLabel) $optgroups['system']['name']['label'] = $nameLabel;
}
}
if($settings['showModifiers']) {
$optgroups['modifier'] = $this->modifierFields;
}
// build system fields optgroup
foreach($optgroups as $optgroupName => $fields) {
$out = '';
foreach($fields as $name => $field) {
if(in_array($name, $settings['exclude'])) continue;
if(count($settings['limitFields']) && !in_array($name, $settings['limitFields'])) continue;
if($name == 'template' && count($settings['templates']) == 1) continue; // if only 1 template allowed, don't allow selection
$hasSubfields = substr($name, -1) == '.';
if(!$settings['showSubfields'] && $hasSubfields) continue;
$_name = $name; // $_name is for output only
if($hasSubfields) $_name = rtrim($_name, '.') . $this->subfieldIdentifier;
$label = $field['label'];
if($name == 'title' && $optgroupName == 'system' && $this->initTemplate) {
$contextField = $this->initTemplate->fieldgroup->getFieldContext($name);
if($contextField) $label = $contextField->label;
}
if($settings['showFieldLabels'] == 2) $label .= " [$name]";
if($hasSubfields) $label .= $this->subfieldIdentifier;
$selected = $selectedValue == $name ? ' selected' : '';
if($name === 'parent' && empty($selected)) continue; // we only show "parent." unless "parent" it was somehow already selected
if($_name == '_custom') $_name = strtolower($label); // always use label for _custom
$text = $settings['showFieldLabels'] ? $label : $_name;
$o = "<option$selected " .
"value='$name' " .
"data-templates='*' " .
"data-name='$_name' " .
"data-label='$label'" .
">$text</option>";
$out .= $o;
$outAll[trim($name, '_')] = $o;
}
$outSections[$optgroupName] = $out;
unset($out);
}
if($settings['showFieldLabels']) {
$customFields = array();
foreach($settings['customFields'] as $field) {
/** @var Field $field */
$label = $field->getLabel();
while(isset($customFields[$label])) $label .= ' ';
$customFields[$label] = $field;
}
ksort($customFields);
} else {
$customFields = $settings['customFields'];
}
// build custom fields optgroup
foreach($customFields as $field) {
/** @var Field $field */
$fieldName = $field->name;
if(in_array($fieldName, $settings['exclude'])) continue;
if(count($settings['limitFields']) && !in_array($fieldName, $settings['limitFields'])) continue;
if(($field->flags & Field::flagSystem) && !$this->allowSystemCustomFields) continue;
if(count($settings['templates'])) {
$allow = 0;
foreach($settings['templates'] as $templateName) {
$template = $this->wire('templates')->get($templateName);
if($template && $template->fieldgroup->hasField($field)) {
$_field = $template->fieldgroup->getFieldContext($field);
if($_field) $field = $_field;
if($field->viewable()) $allow++;
}
}
if(!$allow) continue;
} else {
if(!$field->viewable()) continue;
}
if($this->initTemplate) {
$f = $this->initTemplate->fieldgroup->getField($fieldName, true);
if($f) $field = $f;
}
if($limitSubfields) {
$_settings = $settings;
$_settings['showSubfields'] = !empty($limitSubfields[$fieldName]);
$data = $this->renderSelectFieldOption($field, $_settings, $selectedValue);
} else {
$data = $this->renderSelectFieldOption($field, $settings, $selectedValue);
}
foreach($data as $key => $o) {
if($key == 'all') {
$outAll = array_merge($outAll, $o);
} else {
$outSections[$key] .= $o;
}
}
}
$dataLabels = $this->_('Switch to field labels');
$dataNames = $this->_('Switch to field names');
$dataDefault = $settings['showFieldLabels'] ? $dataNames : $dataLabels;
$outSections['adjust'] = "<option data-labels='$dataLabels' data-names='$dataNames' value='toggle-names-labels'>$dataDefault</option>";
$inputName = $this->attr('name') . "__" . $settings['name'] . "[]";
$selectClass = trim("$this->selectClass select-$settings[name]");
$out = "<select class='$selectClass' name='$inputName' data-type='$settings[type]' data-selected='$selectedValue'>$settings[prepend]";
if(!$this->showOptgroups) {
ksort($outAll);
foreach($outAll as $o) $out .= $o;
$this->optgroupsOrder = 'modifier,adjust';
}
foreach(explode(',', $this->optgroupsOrder) as $name) {
$name = strtolower(trim($name));
if(empty($outSections[$name])) continue;
$optgroupLabel = $outLabels[$name];
$out .= "<optgroup label='$optgroupLabel'>$outSections[$name]</optgroup>";
}
$out .= "$settings[append]</select>";
return $out;
}
/**
* Given a field, return a "|" separated string of template IDs using that field
*
* @param Field $field
* @return string
*
*/
protected function getTemplateIdsUsingField(Field $field) {
static $fieldsToTemplates = array();
if(isset($fieldsToTemplates[$field->name])) return $fieldsToTemplates[$field->name];
$templateIDs = array();
foreach($this->wire('templates') as $template) {
if($template->fieldgroup->hasField($field)) $templateIDs[] = $template->id;
}
$idStr = implode('|', $templateIDs);
$fieldsToTemplates[$field->name] = $idStr;
return $idStr;
}
/**
* Render a single <option> for a <select>
*
* @param Field $field
* @param array $settings
* @param string $selectedValue Optional, default=''
* @return array
*
*/
protected function renderSelectFieldOption(Field $field, array $settings, $selectedValue = '') {
if($field->type instanceof FieldtypeFieldsetOpen) return array();
$fieldSelected = $field->name == $selectedValue ? ' selected' : '';
$templatesStr = $this->getTemplateIdsUsingField($field);
$selectorInfo = $this->getSelectorInfo($field);
if(empty($selectorInfo)) return array();
$out = array(
'field' => '',
'subfield' => '', // no longer used (now replaces 'field')
'group' => '',
'all' => array()
);
$label = $this->wire('sanitizer')->entities($field->getLabel());
if($settings['showFieldLabels'] == 2) $label .= " [$field->name]";
$text = $settings['showFieldLabels'] ? $label : $field->name;
if($selectorInfo['input'] != 'none' && count($selectorInfo['operators'])) {
$o = "<option$fieldSelected " .
"value='$field->name' " .
"data-templates='|$templatesStr|' " .
"data-name='$field->name' " .
"data-label='$label'" .
">$text</option>";
$out['field'] = $o;
$out['all']["$field->name 1"] = $o;
}
if(!empty($settings['showSubfields'])) {
$hasSubfields = count($selectorInfo['subfields']) > 0;
$subfieldSelected = "$field->name." == $selectedValue || $fieldSelected !== '' ? ' selected' : '';
if($settings['showFieldLabels'] == 2) $label .= " [$field->name]";
$option = "<option$subfieldSelected " .
"value='$field->name.' " .
"data-templates='|$templatesStr|' " .
"data-name='$field->name{$this->subfieldIdentifier}' " .
"data-label='$label{$this->subfieldIdentifier}'" .
">$text{$this->subfieldIdentifier}</option>";
$blankValue = $field->type->getBlankValue(new NullPage(), $field);
$isPageField = $blankValue instanceof PageArray || $blankValue instanceof Page;
if($hasSubfields) {
// $out['subfield'] .= $option; // no longer used
$out['field'] = $option; // replace field selection with the subfield version
$out['all']["$field->name 2"] = $option;
}
if(!empty($settings['showSubfieldGroups']) && $isPageField && !$field->get('derefAsPage')) {
$name = $field->name . ' ' . $this->groupIdentifier . $this->subfieldIdentifier;
$label .= ' ' . $this->groupIdentifier . $this->subfieldIdentifier;
$text .= ' ' . $this->groupIdentifier . $this->subfieldIdentifier;
$groupSelected = "@$field->name." == $selectedValue ? ' selected' : '';
$o = "<option$groupSelected " .
"value='@$field->name.' " .
"data-templates='|$templatesStr|' " .
"data-name='$name' " .
"data-label='$label'" .
">$text</option>";
$out['group'] .= $o;
$out['all']["$field->name 3"] = $o;
}
}
return $out;
}
/**
* Render the operator <select>
*
* @param $name
* @param string $type Optional, default=''
* @param string $selectedValue Optional, default=''
* @param array $operators Optional, default=array()
* @return string
*
*/
protected function renderSelectOperator($name, $type = '', $selectedValue = '', $operators = array()) {
$inputName = $this->attr('name') . "__operator[]";
if(!count($operators)) {
if($type && isset($this->systemFields[$name]) && isset($this->systemFields[$name]['operators'])) {
$operators = $this->systemFields[$name]['operators'];
} else if($type && isset($this->modifierFields[$name]) && isset($this->modifierFields[$name]['operators'])) {
$operators = $this->modifierFields[$name]['operators'];
} else if($type && isset($this->operatorsByType[$type])) {
$operators = $this->operatorsByType[$type];
} else {
$operators = $this->operators;
}
}
if($selectedValue === '""' && !in_array($selectedValue, $operators)) {
// if isEmpty/isNotEmpty not supported then convert it to regular equals/notEquals
$selectedValue = rtrim($selectedValue, '"');
}
$disabled = count($operators) ? "" : " disabled='disabled'";
$selectClass = trim("$this->selectClass select-operator");
$out = "<select$disabled class='$selectClass' name='$inputName'>";
foreach($operators as $key => $label) {
if(isset($this->operators[$label])) {
$operator = $label;
$label = $this->operators[$label];
} else {
$operator = $key;
}
$operator = ltrim($operator, $this->operatorTrimChars);
//$label .= " ($operator)";
$selected = $operator === $selectedValue ? ' selected' : '';
$out .= "<option$selected value='" . $this->wire('sanitizer')->entities($operator) . "'>$label</option>";
}
$out .= "</select>";
return $out;
}
/**
* Render the operator <select> and value <input> or <select> or autocomplete
*
* @param $fieldName
* @param string $type
* @param string $selectedOperator
* @param string $selectedValue
* @param bool $orChecked
* @param Selector $selector
* @return string
*
*/
protected function renderOpval($fieldName, $type = '', $selectedOperator = '', $selectedValue = '', $orChecked = false, Selector $selector = null) {
/*
$this->message("fieldName: $fieldName");
$this->message("type: $type");
$this->message("selectedOperator: $selectedOperator");
$this->message("selectedValue: $selectedValue");
$this->message("orChecked: $orChecked");
*/
/** @var Templates $templates */
$templates = $this->wire('templates');
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
if($selector) {}
$field = null;
$inputName = $this->attr('name') . "__value[]";
$options = array();
$out = '';
$selectedValueEntities = $sanitizer->entities($selectedValue);
$operators = array();
$subfield = '';
$placeholder = '';
$_type = ''; // previous type, if changed
$inputfield = null;
$lastTemplates = $this->initTemplate ? array($this->initTemplate->id) : $this->sessionGet('lastTemplates');
if(strpos($fieldName, '.') !== false) list($fieldName, $subfield) = explode('.', $fieldName, 2);
if(isset($this->systemFields[$fieldName]) && !$subfield) {
// system field
$info = $this->systemFields[$fieldName];
if($fieldName == 'parent' && !empty($lastTemplates)) {
// allow for selection of parent, when the template of items is known
//$operators = $this->operatorsByType['page'];
$info['input'] = 'select';
$info['options'] = array();
foreach($lastTemplates as $templateID) {
$template = $templates->get($templateID);
if(!$template) continue;
foreach($this->getParentPages($template) as $p) {
//if(!$this->wire('user')->hasPermission('page-view', $p)) continue;
$info['options'][$p->id] = $p->get('path');
}
}
asort($info['options']);
if(empty($info['options'])) unset($info['options']);
}
$type = $info['input'];
if(isset($info['options'])) $options = $info['options'];
if(!empty($info['placeholder'])) $placeholder = $info['placeholder'];
if($fieldName == 'template' && $selectedValue && !ctype_digit("$selectedValue")) {
$template = $templates->get($sanitizer->name($selectedValue));
if($template) $selectedValue = $template->id;
}
} else if(isset($this->modifierFields[$fieldName])) {
// modifier field
$info = $this->modifierFields[$fieldName];
$type = $info['input'];
if(isset($info['options'])) $options = $info['options'];
if($fieldName == 'sort' && substr($selectedValue, 0, 1) == '-') {
$selectedOperator = '=-';
$selectedValue = ltrim($selectedValue, '-');
}
} else if($type == 'selector') {
// selector field: do nothing here but skip it, and respond to it further down
} else if(($fieldName == 'parent' && $subfield) || $field = $this->wire('fields')->get($fieldName)) {
/** @var Field $field */
// custom field or parent with subfield
$selectorInfo = $this->getSelectorInfo($field);
if(count($selectorInfo) || $fieldName == 'parent') {
// information available for custom field or parent
if($fieldName == 'parent') {
// if parent, there is a subfield, and $field becomes the subfield
$selectorInfo = $this->getSelectorInfo($subfield);
$field = $this->wire('fields')->get($subfield);
} else if($subfield && isset($selectorInfo['subfields'][$subfield])) {
$selectorInfo = $selectorInfo['subfields'][$subfield];
}
$type = $selectorInfo['input'];
if($type == 'page') {
// page field requires special treatment
if($subfield) {
// if there is a subfield, focus on that instead
$field = $this->wire('fields')->get($subfield);
}
if(strlen($selectedValue) && !ctype_digit("$selectedValue") && Selectors::stringHasOperator($selectedValue)) {
// existing value already has a selector in it, so we can assume type=selector
$_type = $type;
$type = 'selector';
} else if(!empty($selectorInfo['options'])) {
// this field specifies it's own options, so we'll use them
$options = $selectorInfo['options'];
} else if($this->useAutocomplete($field)) {
// too many selectable options so use autocomplete
$_type = $type;
$type = 'autocomplete';
} else {
// page field, non-autocomplete, no options specified, and no existing selector string exists
$page = $this->wire('pages')->newNullPage();
$inputfield = $field->getInputfield($page, $field);
if($inputfield instanceof InputfieldPage) {
// get selectable options from InputfieldPage
$collapsed = $inputfield->getSetting('collapsed');
$inputfield->collapsed = Inputfield::collapsedNo;
foreach($inputfield->getSelectablePages($page) as $item) {
$options[$item->id] = $inputfield->getPageLabel($item); // $item->get('title|name');
}
$inputfield->collapsed = $collapsed;
if(count($options) < 2
&& ($field->get('parent_id') || $field->get('template_id'))
&& ($field->get('findPagesCode') || $field->get('findPagesSelector') || $field->get('findPagesSelect'))) {
// see if we can locate options purely with the parent or template
$findSelector = array("include=unpublished, limit=500, sort=title, sort=name, ");
$parent_ids = $field->get('parent_ids');
$template_ids = $field->get('template_ids');
if($parent_ids && count($parent_ids)) {
$findSelector[] = "parent_id=" . implode('|', $parent_ids);
} else if($field->get('parent_id')) {
$findSelector[] = "parent_id=" . (int) $field->get('parent_id');
}
if($template_ids && count($template_ids)) {
$findSelector[] = "templates_id=" . implode('|', $template_ids);
} else if($field->get('template_id')) {
$findSelector[] = "templates_id=" . (int) $field->get('template_id');
}
foreach($this->wire('pages')->find(implode(', ', $findSelector)) as $item) {
$options[$item->id] = $inputfield->getPageLabel($item); // $item->get('title|name');
}
}
} else {
// something other than InputfieldPage, and we don't know the selectable options
// unless the selectorInfo has them
$_type = $type;
$type = 'selector';
}
}
} else {
// non-page custom field
if(!empty($selectorInfo['operators'])) $operators = $selectorInfo['operators'];
if(!empty($selectorInfo['options'])) $options = $selectorInfo['options'];
}
} else {
// no selector info available, only should occur pre-2.4.2
$_type = $type;
$type = 'text';
}
} else {
// unknown field: throw an exception or just let them enter some text
$_type = $type;
$type = 'text';
}
// determine which operators should be used
if($selectedValue == '""' && in_array($selectedOperator, array('=', '!='))) {
// add blank quoted string to operator, to match up with our "has value" or "does not have value" operators
$selectedOperator .= '""';
$selectedValue = '';
$selectedValueEntities = '';
} else if($type == 'selector') {
// populate operators for the selector type
$operators = $this->operatorsByType['selector'];
} else if($type == 'checkbox') {
// populate operators and options for the checkbox type
$operators = $this->operatorsByType['checkbox'];
$options = array(
0 => $this->_('Not Checked'),
1 => $this->_('Checked')
);
} else if($_type == 'page') {
$operators = $this->operatorsByType['page'];
}
// render the OPERATOR selection
$out .= $this->renderSelectOperator($fieldName, $type, $selectedOperator, $operators) . " ";
// render the VALUE input
if(in_array($type, array('select', 'page', 'checkbox'))) {
// render a <select> box for the value
$selectClass = trim("$this->selectClass select-value input-value");
$out .= "<select class='$selectClass' name='$inputName'>";
$out .= "<option value=''></option>";
if($type != 'checkbox' && !isset($this->systemFields[$fieldName]) && !isset($this->modifierFields[$fieldName])) {
// allow for a "None" option to find pages that have no selections for the field
$none = '0';
$hasNone = false;
foreach($options as $value => $label) {
// if select is using "0" as a literal selectable value, don't consider it as "None"
if($value === 0 || $value === '0') {
$none = '""';
} else if($value === '""' || $value === "''") {
// if it already literal quoted blank string then we should not show a none value at all
$hasNone = true;
break;
}
}
if(!$hasNone) {
$selected = $selectedValue == $none || $selectedValue == '""' ? ' selected' : '';
$out .= "<option$selected value='$none'>" . $this->_('None') . "</option>";
}
}
// render each option
foreach($options as $value => $label) {
$selected = $selectedValue == $value && strlen($selectedValue) == strlen($value) ? ' selected' : '';
$out .= "<option$selected value='$value'>$label</option>";
}
$out .= "</select>";
} else if($type == 'autocomplete') {
// render autocomplete input
$placeholder = $this->_('Start typing...');
$selectedValueTitle = '';
if($selectedValueEntities) {
$selectedValuePage = $this->wire('pages')->get((int) $selectedValueEntities);
if($selectedValuePage->id) {
$selectedValueTitle = $selectedValuePage->get('title|name');
if($_type == 'page' && $field) {
$inputfield = $field->getInputfield($this->wire('pages')->newNullPage(), $field);
if($inputfield instanceof InputfieldPage) {
/** @var InputfieldPage $inputfield */
$selectedValueTitle = $sanitizer->entities1($inputfield->getPageLabel($selectedValuePage));
}
}
}
}
$out .= "<input value='$selectedValueEntities' class='input-value' name='$inputName' type='hidden' />";
$inputClass = trim("$this->inputClass input-value-autocomplete");
$out .= "<input value='$selectedValueTitle' class='$inputClass' name='autocomplete_$inputName' placeholder='$placeholder' type='text' />";
} else if($type == 'datetime' || $type == 'date') {
// render date/datetime input
$useTime = $type == 'datetime';
if(!$useTime && $field && $field->get('timeInputFormat')) $useTime = true;
$out .= $this->renderDateInput($inputName, $selectedValue, $useTime);
} else {
// render other input that uses an <input type='text'> whether text, number or selector
$inputType = $type;
$inputClass = trim("$this->inputClass input-value input-value-$type");
if($type == 'number' || $type == 'selector' || $fieldName == 'id' || $subfield == 'id') {
// adding this class tells InputfieldSelector.js that selector strings are allowed for this input
$inputClass .= " input-value-subselect";
$inputType = 'text';
}
$out .= "<input value='$selectedValueEntities' class='$inputClass' name='$inputName' type='$inputType' placeholder='$placeholder' />";
}
// end the opval row by rendering a checkbox for the OR option
$orLabel = $this->_('Check box to make this row OR rather than AND');
$orChecked = $orChecked ? ' checked' : '';
$checkboxClass = trim("$this->checkboxClass input-or");
$out .= "<input$orChecked class='$checkboxClass' type='checkbox' name='or_$inputName' value='1' title='$orLabel' />";
return $out;
}
/**
* Render a datepicker input
*
* @param $name
* @param $value
* @param bool $useTime
* @return mixed
*
*/
protected function renderDateInput($name, $value, $useTime = false) {
$inputfield = $this->wire('modules')->get('InputfieldDatetime');
$inputfield->attr('name', $name);
$inputfield->datepicker = InputfieldDatetime::datepickerFocus;
$inputfield->placeholder = $this->datePlaceholder;
$inputfield->dateInputFormat = $this->dateFormat;
$inputfield->addClass('input-value');
if($useTime) {
$inputfield->timeInputFormat = $this->timeFormat;
$inputfield->placeholder .= ' ' . $this->timePlaceholder;
}
$inputfield->attr('value', $value);
$inputfield->renderReady();
return $inputfield->render();
}
/**
* Render a subfield <select>
*
* @param $fieldName
* @param string $selectedValue
* @param Selector $selector
* @return string
*
*/
protected function renderSelectSubfield($fieldName, $selectedValue = '', Selector $selector = null) {
if(strpos($fieldName, '.')) list($fieldName,) = explode('.', $fieldName, 2);
$valueLabel = $this->_('Value');
$valueName = strtolower($valueLabel);
if($fieldName == 'parent' || $fieldName == 'children') {
// for parent or children, use the existing functionality in renderSelectField
$fields = $this->wire('fields');
$out = $this->renderSelectField(array(
'name' => 'subfield',
'class' => 'select-subfield',
'type' => 'page',
'showModifiers' => false,
'showSubfields' => false,
'customFields' => $fields,
'exclude' => array(), // prevent exclusion of 'count'
'prepend' =>
"<option value='parent' class='select-subfield-default' data-name='$valueName' data-label='$valueLabel'>" .
$valueLabel .
"</option>",
), $selectedValue);
return $out;
}
if(isset($this->systemFields[$fieldName])) {
// system fields (other than parent|children) don't have subfield selections
return '';
}
// check if this is a custom field
$field = $this->wire('fields')->get($fieldName);
if(!$field) return "Unknown Field: $fieldName"; // shouldn't happen, but for debugging
// if we've reached this point we're dealing with a custom field
$selectorInfo = $this->getSelectorInfo($field);
$inputName = $this->attr('name') . "__" . $field->name . "[]";
$outSub = ''; // subselector optgroup output
$selectClass = trim("$this->selectClass select-subfield");
$out = "<select class='$selectClass' name='$inputName' data-type='$selectorInfo[input]'>";
//$out .= "<option></option>";
$out .= "<option " .
"class='select-subfield-default' " .
"value='$field->name' " .
"data-name='$valueName' " .
"data-label='$valueLabel' " .
">$valueLabel</option>" .
"<option disabled></option>";
// determine if there is a current value string and if it contains a selector string
$selectorValue = is_null($selector) ? '' : $selector->value;
if(is_array($selectorValue)) $selectorValue = reset($selectorValue);
$valueHasSelectorString = strlen($selectorValue) > 0 && Selectors::stringHasSelector($selectorValue);
$limitSubfields = array();
if(is_array($this->limitFields)) foreach($this->limitFields as $limitField) {
if(strpos($limitField, '.') === false) continue;
if(strpos($limitField, $fieldName) !== 0) continue;
list($limitField, $limitSubfield) = explode('.', $limitField);
if($limitField) {} // ignore
if($limitSubfield) $limitSubfields[$limitSubfield] = $limitSubfield;
}
// render all the subfield options
foreach($selectorInfo['subfields'] as $name => $info) {
if(count($limitSubfields) && !isset($limitSubfields[$name])) continue;
$label = $this->wire('sanitizer')->entities($info['label']);
// render primary subfield selection (unless selector info says not to)
if(isset($info['input']) && $info['input'] != 'none') {
$selected = $selectedValue == $name && (!$valueHasSelectorString || empty($info['subfields'])) ? ' selected' : '';
$out .= "<option$selected " .
"value='$field->name.$name' " .
"data-name='$name' " .
"data-label='$label' " .
">$label</option>";
}
// render 'id' option if there are subfields: this enables one to specify a sub-selector string
// since selectors don't allow things like field.subfield.tertiaryfield
if(!empty($info['subfields']) && $this->allowSubselectors) {
$selected = $selectedValue == $name && $valueHasSelectorString ? ' selected' : '';
$outSub .= "<option$selected " .
"value='$field->name.$name.id' " .
"data-name='$name.id' " .
"data-label='$label'" .
">$label</option>";
}
}
if($outSub) {
$label = $this->_('Match by ID (subselector)');
$out .= "<optgroup label='$label'>$outSub</optgroup>";
}
$out .= "</select>";
return $out;
}
/**
* Whether or not to use autocomplete
*
* If no, blank string is returned.
* If yes, then the selector string to find pages is returned.
*
* @param Field $field
* @param int $threshold If determined selectable quantity is <= this number, function will return blank. (default=-1 to use configured maxSelectOptions)
* @param bool $checkQuantity
* @return string Selector string. Blank string means don't use autocomplete.
*
*/
protected function useAutocomplete(Field $field, $threshold = -1, $checkQuantity = true) {
if($threshold === -1) $threshold = (int) $this->maxSelectOptions;
//$selectorInfo = $this->getSelectorInfo($field);
if(!$field->type instanceof FieldtypePage) return '';
$selector = '';
$hasPageListSelect = strpos($field->get('inputfield'), 'PageListSelect') !== false;
// determine autocomplete state based on field settings and quantity of pages involved
$findPagesSelector = $field->get('findPagesSelector|findPagesSelect');
if($findPagesSelector) {
// user-specified selector determines which pages match
$selector = trim($findPagesSelector, ', ');
if(strpos($selector, 'page.') !== false) {
// remove page.something impossible reference, if present
$selector = preg_replace('/[_a-zA-Z0-9]+[=<>!]+page\.[_a-zA-Z0-9]+[\s,]*/', '', $selector);
//$selector = preg_replace('/(^|,)\s*page\.[_a-zA-Z0-9][=<>!]+[^,]*/', '', $selector);
}
if($field->get('parent_id')) $selector .= ",has_parent=" . (int) $field->get('parent_id');
} else if($field->get('parent_id')) {
if($hasPageListSelect) {
$selector = "has_parent=" . (int) $field->get('parent_id');
} else {
$selector = "parent_id=" . (int) $field->get('parent_id');
}
}
if($field->get('template_id')) {
$selector .= ",templates_id=";
if(is_array($field->get('template_id'))) {
if(count($field->get('template_id'))) $selector .= implode('|', $field->get('template_id'));
} else {
$selector .= (int) $field->get('template_id');
}
$templateIDs = $field->get('template_ids');
if(is_array($templateIDs) && count($templateIDs)) $selector .= "|" . implode('|', $templateIDs);
}
if(empty($selector)) {
// if it's using a runtime code to determine, then we can't use autocomplete
if($field->get('findPagesCode')) return '';
// otherwise just populate a selector that can match anything
$selector = "id>0";
}
if(!$checkQuantity) return $selector;
if($hasPageListSelect) return $selector;
$quantity = $this->wire('pages')->count($selector);
return $quantity > $threshold ? $selector : '';
}
/**
* Render autocomplete results and return JSON string
*
* @param string $fieldName
* @param string $q Query string
* @return string JSON
*
*/
protected function renderAutocompleteJSON($fieldName, $q) {
header("Content-Type: application/json");
// format for our returned JSON
$data = array(
'field' => "$fieldName",
'status' => 0, // 0=error, 1=success
'selector' => '',
'items' => array()
);
$selector = '';
$subfield = '';
if(strpos($fieldName, '.') !== false) {
list($fieldName, $subfield) = explode('.', $fieldName, 2);
}
$field = $this->wire('fields')->get($fieldName);
$selectorInfo = $this->getSelectorInfo($field);
if($subfield && isset($selectorInfo['subfields'][$subfield])) {
$selectorInfo = $selectorInfo['subfields'][$subfield];
if($selectorInfo['input'] == 'autocomplete' && isset($selectorInfo['selector'])) {
$selector = $selectorInfo['selector'];
}
}
if(!$selector && $subfield) {
$fieldName = $subfield;
$field = $this->wire('fields')->get($fieldName);
}
if(!$field) {
$data['error'] = 'Field does not exist: ' . $fieldName;
return json_encode($data);
}
if(!$selector) $selector = $this->useAutocomplete($field, $this->maxSelectOptions, false);
if(!$selector) {
$data['error'] = "Field '$field->name' does not require autocomplete";
return json_encode($data);
}
$searchFields = $field->searchFields; // used by InputfieldPageAutocomplete
$labelFieldName = $field->labelFieldName;
$labelFieldFormat = $field->labelFieldFormat;
if(!$searchFields && isset($selectorInfo['searchFields'])) $searchFields = $selectorInfo['searchFields'];
if(!$labelFieldName && !$labelFieldFormat && isset($selectorInfo['labelFieldName'])) {
$labelFieldName = $selectorInfo['labelFieldName'];
}
$labelField = $labelFieldName ? $this->wire('fields')->get($labelFieldName) : null;
$template_id = (int) (is_array($field->template_id) ? reset($field->template_id) : $field->template_id);
$template = $template_id ? $this->wire('templates')->get($template_id) : null;
if($labelFieldFormat) {
/** @var InputfieldPage $inputfield */
$inputfield = $field->getInputfield(new NullPage(), $field);
} else {
$inputfield = null;
}
if($searchFields) {
$searchFields = str_replace(array(', ', ',', ' '), '|', trim($searchFields));
} else if($labelField && $labelField->type instanceof FieldtypeText) {
$searchFields = $labelFieldName;
} else if($template && $template->fieldgroup->hasField('title')) {
$searchFields = 'title';
$labelFieldName = 'title';
} else {
$searchFields = 'name';
$labelFieldName = 'name';
}
$selector .= ", $searchFields%=" . $this->wire('sanitizer')->selectorValue($q);
$selector .= ", limit=50, include=hidden";
foreach($this->wire('pages')->find($selector) as $item) {
$label = $inputfield ? $inputfield->getPageLabel($item) : $item->get("$labelFieldName|name");
$data['items'][] = array(
'value' => $item->id,
'label' => $label
);
}
$data['status'] = 1;
$data['selector'] = $selector;
return json_encode($data);
}
/**
* Render a selector row <li>
*
* @param string $select Rendered output for the <select>
* @param string $subfield Rendered output for the subfield <select> if applicable
* @param string $opval Rendered output for the operator <select> and value <input>
* @param string $class Optional class for the row
* @return string
*
*/
public function renderRow($select, $subfield, $opval, $class = '') {
if($this->allowAddRemove) {
$delete = "<a href='#' class='delete-row'><i class='fa fa-trash-o'></i></a>";
} else {
$delete = '';
}
$out = "<li class='selector-row ui-helper-clearfix $class'>
$select
<span class='subfield'>$subfield</span>
<span class='opval'>$opval</span> &nbsp;
$delete
</li>
";
return $out;
}
/**
* Set an attribute to this Inputfield, overridden from Inputfield class
*
* @param array|string $key
* @param int|string $value
* @return InputfieldSelector|WireData
*
*/
public function setAttribute($key, $value) {
if($key == 'value') {
if($this->initValue && strpos($value, $this->initValue) === 0) {
// remove initValue from value so that inputs aren't drawn for it
$value = substr($value, strlen($this->initValue));
$value = trim($value, ', ');
// @todo solve this scenario, which leaves you with just "29"
// value: template=29
// initValue: template=
}
$this->lastSelector = trim("$this->initValue, $value", ', ');
if($this->initTemplate) {
$lastTemplates = array($this->initTemplate->id);
} else if(strpos($this->lastSelector, 'template=') !== false) {
$lastTemplates = array();
foreach(new Selectors($this->lastSelector) as $item) {
if($item->field != 'template') continue;
foreach($item->values as $templateValue) {
$template = $this->wire('templates')->get($templateValue);
if($template && $template instanceof Template) $lastTemplates[] = $template->id;
}
}
} else {
$lastTemplates = array();
}
$this->sessionSet('lastTemplates', $lastTemplates);
}
return parent::setAttribute($key, $value);
}
/**
* Set property
*
* @param string $key
* @param mixed $value
* @return $this|Inputfield|WireData
* @throws WireException
*
*/
public function set($key, $value) {
if($key === 'initTemplate') {
if($value && !$value instanceof Template) {
$value = $this->wire('templates')->get($value);
}
if($value === null || $value instanceof Template) {
$this->initTemplate = $value;
return $this;
} else {
throw new WireException('initTemplate must be a Template or null');
}
}
return parent::set($key, $value);
}
/**
* Get property
*
* @param string $key
* @return mixed
*
*/
public function get($key) {
if($key === 'initTemplate') return $this->initTemplate;
return parent::get($key);
}
/**
* Makes sure that existing fields present in selector are added to limitFields
*
*/
protected function prepareLimitFields() {
if(!count($this->limitFields)) return;
$limitFields = $this->limitFields;
foreach($this->selectors as $selector) {
foreach($selector->fields as $field) {
// if limitFields is in use and a field is present in the selector that isn't present
// in limitFields then make sure it is added to limitFields
if(!in_array($field, $limitFields)) {
$limitFields[] = $field;
}
}
}
$this->limitFields = $limitFields;
}
/**
* Primary Inputfield render method
*
* @return string
*
*/
public function ___render() {
$this->ready();
// tell jQuery UI we want it to load the modal component which makes a.modal open modal windows
$this->wire('modules')->get('JqueryUI')->use('modal');
if(self::debug) $this->counter = true;
// force load the CSS/JS files used for dates, since we don't know if they will be needed or not
$this->renderDateInput('tmp', '', true);
// build the structures and default values
$this->setup();
$this->wrapClass .= ' InputfieldSelector_' . ($this->showFieldLabels ? 'labels' : 'names');
// convert the value attribute to a Selectors object
try {
$value = trim($this->attr('value'));
$this->selectors = $this->wire(new Selectors());
if(!$this->parseVars) $this->selectors->setParseVars(false);
$this->selectors->setSelectorString($value);
} catch(\Exception $e) {
$this->error($e->getMessage());
}
$this->prepareLimitFields();
// set a session variable so that ajax request know there has been a valid request
$this->sessionSet('valid', true);
$this->sessionSet('initValue', $this->initValue);
// all other session variables that need to be remembered
foreach($this->sessionVarNames as $key) {
$this->sessionSet($key, $this->$key);
}
// determine if there are any initValue templates in play, so that we can pre-limit what fields are available
$templates = array();
$renderSelectOptions = array();
if($this->initValue) foreach(new Selectors($this->initValue) as $selector) {
if($selector->field == 'template') {
$templateValue = $selector->value;
if(!is_array($templateValue)) $templateValue = array($templateValue);
foreach($templateValue as $t) {
$template = $this->wire('templates')->get($t);
if($template) {
$templates[] = $template->name;
if(count($templateValue) == 1) $this->initTemplate = $template;
}
}
}
if(count($templates)) $renderSelectOptions['templates'] = $templates;
}
$select = $this->renderSelectField($renderSelectOptions);
$previewClass = $this->preview ? '' : ' selector-preview-disabled';
$counterClass = $this->counter ? '' : ' selector-counter-disabled';
// render the template row to start: this is duplicated with JS when a field added
$rows = $this->renderRow($select, '', '', 'selector-template-row');
// render all the rows for existing selector values already in this Inputfield's value
foreach($this->selectors as $selector) {
/** @var Selector $selector */
$rowClass = '';
$orChecked = false;
$fields = $selector->fields;
$quote = $selector->quote;
if(!$this->parseVars && $this->selectors->selectorHasVar($selector)) {
// allow for "_custom (field=value)" where value contains API var reference
$selector->value = implode('|', $fields) . "$selector->operator[$selector->value]";
$fields = array('_custom');
}
// render a row for each field in the $selector (usually 1)
foreach($fields as $fieldNum => $field) {
$field1 = $field;
$field2 = '';
$group = is_null($selector->group) ? '' : '@';
$dot = strpos($field, '.');
if($dot && !isset($this->systemFields[$field])) {
$field1 = substr($field, 0, $dot);
$field2 = substr($field, $dot+1);
$hasSubfields = true;
/*
} else if($field1 === 'parent') {
$hasSubfields = true;
if($dot) $field2 = substr($field, $dot + 1);
// else $field2 = '.';
*/
} else {
$selectorInfo = $this->getSelectorInfo($field1);
$hasSubfields = $selectorInfo && isset($selectorInfo['subfields']) ? count($selectorInfo['subfields']) : false;
}
$select = $this->renderSelectField(array(), $group . $field);
$select2 = $hasSubfields ? $this->renderSelectSubfield($field1, $field2, $selector) : '';
if($select2) $rowClass .= " has-subfield";
if($fieldNum > 0) $rowClass .= " has-or-field";
$values = $selector->value;
if(!is_array($values)) $values = array($values);
// render a row for each value in the selector (usually 1)
foreach($values as $valueNum => $value) {
if($valueNum > 0 || $quote == '(') $rowClass .= " has-or-value";
if($fieldNum > 0 || $valueNum > 0 || $quote == '(') $orChecked = true;
if(!strlen($value) && $quote) $value = "$quote{$value}$quote";
$operator = $selector->operator;
// convert to not operator when finding a not selector
if($selector->not && isset($this->operators["!$selector->operator"])) $operator = "!$selector->operator";
//$opval = $this->renderOpval(($field2 ? $field2 : $field1), '', $operator, $value, $orChecked);
$opval = $this->renderOpval($field, '', $operator, $value, $orChecked);
$rows .= $this->renderRow($select, $select2, $opval, $rowClass);
}
}
}
$notes = $this->_('Each selector row above says: field must match value. These are called AND conditions. In cases where the same field or value appears in more than one row, an OR condition is possible. The presence of a checkbox at the end of the row indicates this. Check this box to make the row an OR condition rather than an AND condition.'); // Description of OR checkbox
// attributes for our hidden input, populated by javascript as filters are added/changed/removed
$attr = array(
'type' => 'hidden',
'id' => $this->attr('id'),
'name' => $this->attr('name'),
'value' => $this->attr('value'),
'class' => 'selector-value',
'data-template-ids' => implode(',', $this->getTemplatesFromInitValue($this->initValue)),
);
if($this->allowBlankValues) $attr['class'] .= ' allow-blank';
$attrStr = $this->getAttributesString($attr);
$attr['value'] = $this->wire('sanitizer')->entities($attr['value']);
$initValue = $this->wire('sanitizer')->entities($this->initValue);
// starting output
$out = "
<ul class='selector-list'>
$rows
</ul>
<div class='ui-helper-clearfix'>
";
if($this->allowAddRemove) $out .= "
<a class='selector-add' href='#'><i class='fa fa-$this->addIcon'></i> $this->addLabel</a>
";
$out .= "
<span class='selector-counter$counterClass detail'></span>
<p class='selector-preview$previewClass' data-init-value='$initValue'>$attr[value]</p>
<input $attrStr />
<p class='detail or-notes'>$notes</p>
</div>
";
return $this->prep($out);
}
/**
* Sanitize a selector string and return it
*
* @param $selectorString
* @param bool $parseVars Whether variables like [user.id] should be converted to actual value
* @return string
*
*/
public function sanitizeSelectorString($selectorString, $parseVars = true) {
$sanitizer = $this->wire()->sanitizer;
$users = $this->wire()->users;
$initSelectors = $this->wire(new Selectors());
$userSelectors = $this->wire(new Selectors());
if(!$parseVars) {
$initSelectors->setParseVars(false);
$userSelectors->setParseVars(false);
}
$initSelectors->setSelectorString($this->initValue);
$userSelectors->setSelectorString($selectorString);
foreach($userSelectors as $s) {
if($s->quote == '[' && !$this->allowSubselectors) {
$this->error("Subselectors are disabled");
$userSelectors->remove($s);
$userSelectors->add(new SelectorLessThan('id', 0)); // forced non match
}
if(in_array($s->field, array('modified_users_id', 'created_users_id')) && !ctype_digit("$s->value")) {
$ids = array();
foreach($s->values as $key => $name) {
$property = ctype_digit("$name") ? 'id' : 'name';
$operator = $s->operator === '!=' ? '=' : $s->operator;
if($property === 'name') {
$value = $sanitizer->selectorValue($sanitizer->pageNameUTF8($name));
} else {
$value = (int) $name; // id
if($value === 0) $value = '';
}
if($value !== '') $ids = array_merge($ids, $users->findIDs("$property$operator$value"));
}
if($s->operator != '=' && $s->operator != '!=') {
$userSelectors->remove($s);
$s = new SelectorEqual($s->field, count($ids) ? $ids : '');
$userSelectors->add($s);
} else {
$s->value = count($ids) ? $ids : '';
}
}
}
$selector = (string) $initSelectors . ", ";
$selector .= (string) $userSelectors;
$selector = trim($selector, ", ");
return $selector;
}
/**
* Returns array of template IDs that correspond with any templates specified in the initValue
*
* @param string $initValue
* @return array of template IDs
*
*/
protected function getTemplatesFromInitValue($initValue) {
// determine if a template is enforced and populate allowedTemplates
$templates = array();
if(!$initValue || strpos($initValue, 'template=') === false) return array();
foreach(new Selectors($initValue) as $selector) {
if($selector->field == 'template') {
$value = is_array($selector->value) ? $selector->value : array($selector->value);
foreach($value as $name) {
$t = $this->wire('templates')->get($name);
if($t) $templates[] = $t->id;
}
}
}
return $templates;
}
/**
* Process input submitted to this Inputfield
*
* @param WireInputData $input
* @return $this
*
*/
public function ___processInput(WireInputData $input) {
parent::___processInput($input);
$value = $this->attr('value');
$this->attr('value', $this->sanitizeSelectorString($value, false));
return $this;
}
/**
* Module settings: provide a sandbox area for playing with the Inputfield
*
* @param array $data
* @return InputfieldWrapper
*
*/
public function getModuleConfigInputfields(array $data) {
if($data) {} // ignore
$form = $this->wire(new InputfieldWrapper());
$f = $this->wire('modules')->get('InputfieldSelector');
$f->name = 'test';
$f->label = $this->_('Selector Sandbox');
$f->description = $this->_('This is here just in case you want to test out the functionality of this Inputfield.');
$form->add($f);
return $form;
}
/**
* Get parent pages of pages using the given template
*
* @param Template $template
* @return PageArray
* @throws WireException
*
*/
public function getParentPages(Template $template) {
$templates = $this->wire('templates');
$parentPages = $templates->getParentPages($template, true, Page::statusUnpublished);
if($parentPages->count()) return $parentPages;
$parentIDs = array();
$sql =
'SELECT parent_id FROM pages ' .
'WHERE pages.templates_id=:templates_id ' .
'AND status<' . Page::statusTrash . ' ' .
'GROUP BY parent_id LIMIT 500';
$query = $this->wire('database')->prepare($sql);
$query->bindValue('templates_id', $template->id, \PDO::PARAM_INT);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_NUM)) {
$parentID = (int) $row[0];
$parentIDs[$parentID] = $parentID;
}
$query->closeCursor();
$parentPages = count($parentIDs) ? $this->wire('pages')->getById($parentIDs) : new PageArray();
foreach($parentPages as $parentPage) {
if(!$parentPage->viewable(false)) $parentPages->remove($parentPage);
}
return $parentPages;
}
}