'Page Search',
'summary' => 'Provides a page search engine for admin use.',
'version' => 105,
'permanent' => true,
'permission' => 'page-edit',
);
}
const defaultOperator = '%=';
protected $nativeSorts = array(
'relevance',
'name',
'title',
'id',
'status',
'templates_id',
'parent_id',
'created',
'modified',
'modified_users_id',
'created_users_id',
'createdUser',
'modifiedUser',
'sort',
'sortfield',
);
protected $fieldOptions = array();
protected $customSorts = array();
protected $operators = array();
protected $resultLimit = 25;
protected $maxLimit = 250;
protected $lister = null;
/**
* Mode indicating admin ajax search, set by GET var admin_search=1
*
* This mode typically focuses on just searching the 'title' field.
* This mode also includes data specific to ajax requests.
*
*/
protected $adminSearchMode = false;
public function init() {
foreach($this->fields as $field) {
if($field->type instanceof FieldtypeFieldsetOpen) continue;
if($field->type instanceof FieldtypePassword) continue;
$this->fieldOptions[] = $field->name;
}
sort($this->fieldOptions);
parent::init();
}
static public function getOperators() {
$f = __FILE__;
return array(
'=' => __('Equals', $f),
'!=' => __('Does not equal', $f),
'>' => __('Greater than', $f),
'>=' => __('Greater than or equal to', $f),
'<' => __('Less than', $f),
'<=' => __('Less than or equal to', $f),
'*=' => __('Contains phrase or partial word', $f),
'%=' => __('Contains phrase/word using LIKE', $f),
'~=' => __('Contains all the words', $f),
);
}
/**
* Setup items needed for full execution, as opposed to the regular search input that appears on all pages
*
*/
protected function fullSetup() {
$headline = $this->_x('Search', 'headline'); // Headline for search page
if($this->input->get('processHeadline')) {
$headline = $this->sanitizer->entities($this->sanitizer->text($this->input->get('processHeadline')));
$this->input->whitelist('processHeadline', $headline);
}
$this->wire('processHeadline', $headline);
$this->operators = self::getOperators();
}
/**
* Perform an interactive search and provide a search form (default)
*
*/
public function ___execute() {
if($this->wire('user')->hasPermission('lister')) {
if($this->wire('modules')->isInstalled('ProcessPageLister')) {
$this->lister = $this->wire('modules')->get('ProcessPageLister');
}
}
$ajax = $this->wire('config')->ajax;
if($this->lister && $ajax) {
// we will just let Lister do it's thing, since it remembers settings in session
return $this->lister->execute();
} else {
$this->fullSetup();
$this->processInput();
list($selector, $displaySelector, $initSelector, $defaultSelector) = $this->buildSelector();
}
if($this->lister) {
$lister = $this->lister;
if(count($_GET)) $lister->sessionClear();
$lister->initSelector = $initSelector;
$lister->defaultSelector = $defaultSelector;
$lister->defaultSort = 'relevance';
$lister->limit = $this->resultLimit;
$lister->preview = false;
$lister->columns = $this->getDisplayFields();
return $lister->execute();
} else {
$matches = $this->pages->find($selector);
return $this->render($matches, $displaySelector);
}
}
/**
* Perform a non-interactive search (based on URL GET vars)
*
* This is the preferred input method for links and ajax queries.
*
* Example /search/for?template=basic-page&body*=example
*
*/
public function ___executeFor() {
$this->fullSetup();
$selector = '';
$displaySelector = '';
$limit = $this->resultLimit;
$start = 0;
$status = 0;
$names = array();
$adminSearchStr = '';
foreach($this->input->get as $name => $value) {
// operator has no '=', so we'll get the value from the name
// so that you can do something like: bedrooms>5 rather than bedrooms>=5
if(!strlen($value) && preg_match('/([^<>]+)\s*([<>])\s*([^<>]+)/', $name, $matches)) {
$name = $matches[1];
$operator = $matches[2];
$value = $matches[3];
} else {
$operator = substr($name, -1) . '=';
if(isset($this->operators[$operator])) {
$name = substr($name, 0, -1);
} else {
$operator = '=';
}
}
// replace '-' with '.' since '.' is not allowed in URL variable names
if(strpos($name, '-')) {
list($name, $subname) = explode('-', $name);
$name = "$name.$subname";
}
if(strpos($name, ',')) $name = $this->sanitizer->names($name, ',', array('_', '.'));
else $name = $this->sanitizer->pageName($name); // note: switch to pageName over fieldName to support "."
if(!$name) continue;
if($name == 'limit') {
$limit = (int) $value;
$this->input->whitelist('limit', $value);
continue;
}
if($name == 'start') {
$start = (int) $value;
$this->input->whitelist('start', $value);
continue;
}
// if dealing with a user other than superuser, only allow include=hidden
if($name == 'include' && $value != 'hidden' && !$this->user->isSuperuser()) $value = 'hidden';
// don't allow setting of check_access property, except for superuser
if($name == 'check_access' && !$this->user->isSuperuser()) continue;
// don't allow setting of the 'status' property, except for superuser
if($name == 'status') {
if(!$this->user->isSuperuser()) continue;
$status = (int) $value;
}
// check if adminSearchMode should be enabled (for ajax search)
if($name == 'admin_search') {
$adminSearchStr = $this->sanitizer->selectorValue($value);
$this->adminSearchMode = true;
continue;
}
// replace URL-compatible comma separators with selector-compatible pipes
if(strpos($name, ',')) $name = str_replace(',', '|', $name);
if(!$this->isSelectableFieldName($name)) continue;
if(strpos($value, '|')) {
$values = explode('|', $value);
foreach($values as $k => $v) $values[$k] = $this->sanitizer->selectorValue($v);
$value = implode('|', $values);
} else {
$value = $this->sanitizer->selectorValue($value);
}
$this->input->whitelist($name . rtrim($operator, '='), trim($value, '"\''));
$selector .= "$name$operator$value, ";
$names[] = $name;
}
if(strlen($adminSearchStr)) {
// adminSearchMode active, auto populate a search, like title%=value
$fields = $this->searchFields2 ? explode(' ', $this->searchFields2) : array('title');
$operator = strlen($adminSearchStr) < 4 ? '%=' : '*=';
$selector .= implode('|', $fields) . $operator . $adminSearchStr . ", ";
}
if($start) $selector .= "start=$start, ";
$selector .= "limit=$limit, ";
$selector = rtrim($selector, ", ");
$displaySelector = $selector;
if($this->adminSearchMode && !in_array('has_parent', $names)) {
// exclude repeaters from matching, when present
$admin = $this->wire('pages')->get($this->wire('config')->adminRootPageID);
$repeaters = $admin->children("name=repeaters, include=all");
if(count($repeaters)) $selector .= ", has_parent!=" . $repeaters->first()->id;
}
if($this->user->isSuperuser() && !$status && !preg_match('/\binclude=/', $selector)) {
$selector .= ", include=all, status<" . Page::statusTrash;
}
return $this->render($this->pages->find($selector), $displaySelector);
}
/**
* Return array of fields to display in results
*
*/
protected function getDisplayFields() {
$display = $this->input->get('display');
if(!strlen($display)) $display = $this->input->get('get'); // as required by ProcessPageSearch API
if(!strlen($display)) $display = $this->displayField;
if(!strlen($display)) $display = 'title path';
$display = str_replace(',', ' ', $display);
$display = explode(' ', $display); // convert to array
foreach($display as $key => $name) {
$name = $this->sanitizer->fieldName($name);
$display[$key] = $name;
if($this->isSelectableFieldName($name)) continue;
if(in_array($name, array('url', 'path', 'httpUrl'))) continue;
unset($display[$key]);
}
return $display;
}
/**
* Render the search results
*
*/
protected function render(PageArray $matches, $displaySelector) {
$out = '';
if($displaySelector) $this->message(sprintf($this->_n('Found %1$d page using selector: %2$s', 'Found %1$d pages using selector: %2$s', $matches->getTotal()), $matches->getTotal(), $displaySelector));
// determine what fields will be displayed
$display = $this->getDisplayFields();
$this->input->whitelist('display', implode(',', $display));
if($this->config->ajax) {
// ajax json output
$out = $this->renderMatchesAjax($matches, $display, $displaySelector);
} else {
// html output
$class = '';
if((int) $this->input->get->show_options !== 0 && $this->input->urlSegment1 != 'find') {
$out = "\n
" . $this->renderFullSearchForm() . "
";
$class = 'show_options';
}
$out .= "\n" . $this->renderMatchesTable($matches, $display) . "\n
";
}
return $out;
}
/**
* Build a selector based upon interactive choices from the search form
*
*/
protected function buildSelector() {
$selector = ''; // for regular ProcessPageSearch
$initSelector = ''; // for Lister, non-changable part of the selector
$defaultSelector = ''; // for Lister, changeable filters
// search query text
$q = $this->input->whitelist('q');
if(strlen($q)) {
$searchFields = $this->searchFields;
if(is_string($searchFields)) $searchFields = explode(' ', $searchFields);
foreach($searchFields as $fieldName) {
$fieldName = $this->sanitizer->fieldName($fieldName);
$selector .= "$fieldName|";
}
$selector = rtrim($selector, '|') . $this->operator . $this->wire('sanitizer')->selectorValue($q);
}
// determine if results are sorted by something other than relevance
$sort = $this->input->whitelist('sort');
if($sort && $sort != 'relevance') {
$reverse = $this->input->whitelist('reverse') ? "-" : '';
$selector .= ", sort=$reverse$sort";
// if a specific template isn't requested, then locate the templates that use this field and confine the search to them
if(!$this->input->whitelist('template') && !in_array($sort, $this->nativeSorts)) {
$templates = array();
foreach($this->templates as $template) {
if($template->fieldgroup->has($sort)) $templates[] = $template->name;
}
if(count($templates)) $selector .= ", template=" . implode("|", $templates);
}
}
// determine if search limited to a specific template
if($this->input->whitelist('template')) {
$selector .= ", template=" . $this->input->whitelist('template');
}
if(!$selector) {
$this->error($this->_("No search specified"));
return array('','','','');
}
$selector = trim($selector, ", ");
$displaySelector = $selector; // highlight the selector that was used for display purposes
$defaultSelector = $selector; // user changable selector in Lister
$initSelector = '' ; // non-user changable selector in Lister
$s = ''; // anything added to this will be populated to both $selector and $initSelector below
// limit results for pagination
$s = ", limit={$this->resultLimit}";
$adminRootPage = $this->wire('pages')->get($this->wire('config')->adminRootPageID);
// exclude admin repeater pages unless the admin template is chosen
if(!$this->input->whitelist('template')) {
// but only for superuser, as we're excluding all admin pages for non-superusers
if($this->user->isSuperuser()) {
$repeaters = $adminRootPage->child('name=repeaters, include=all');
if($repeaters->id) $s .= ", has_parent!={$repeaters->id}";
}
}
// include hidden pages
if($this->user->isSuperuser()) {
$s .= ", include=all";
} else {
// non superuser doesn't get any admin pages in their results
$s .= ", has_parent!=$adminRootPage";
// if user has any kind of edit access, allow unpublished pages to be included
if($this->user->hasPermission('page-edit')) $s .= ", include=unpublished";
}
$selector .= $s;
$initSelector .= $s;
return array($selector, $displaySelector, trim($initSelector, ', '), $defaultSelector);
}
/**
* Process input from the search form
*
*/
protected function processInput() {
// search fields
if($this->input->get->field) {
$field = str_replace(',', ' ', $this->input->get->field);
$fieldArray = explode(' ', $field);
$field = '';
foreach($fieldArray as $f) {
$f = $this->sanitizer->fieldName($f);
if(!in_array($f, $this->fieldOptions) && !in_array($f, $this->nativeSorts)) continue;
$field .= $f . " ";
}
$field = rtrim($field, " ");
if($field) {
$this->searchFields = $field;
$this->input->whitelist('field', $field);
}
} else {
$this->input->whitelist('field', $this->searchFields);
}
// operator, search type
if(empty($this->operator)) $this->operator = self::defaultOperator;
$operator = $this->input->get->operator;
if(!is_null($operator)) {
if(array_key_exists($operator, $this->operators)) {
$this->operator = substr($this->input->get->operator, 0, 3);
} else if(ctype_digit("$operator")) {
$operators = array_keys($this->operators);
if(isset($operators[$operator])) $this->operator = $operators[$operator];
}
$this->input->whitelist('operator', $this->operator);
}
// search query
$q = $this->sanitizer->text(substr($this->input->get->q, 0, 128));
$this->input->whitelist('q', $q);
// sort
$this->input->whitelist('sort', 'relevance');
if($this->input->get->sort) {
$sort = $this->sanitizer->fieldName($this->input->get->sort);
if($sort && (in_array($sort, $this->nativeSorts) || in_array($sort, $this->fieldOptions))) $this->input->whitelist('sort', $sort);
if($this->input->get->reverse) $this->input->whitelist('reverse', 1);
}
// template
if($this->input->get->template) {
$template = $this->sanitizer->name($this->input->get->template);
if(!$this->templates->get($template)) $template = '';
if($template) $this->input->whitelist('template', $template);
}
}
/**
* Is the given field name selectable?
*
*/
protected function isSelectableFieldName($name, $level = 0) {
$is = false;
if(!$level && strpos($name, '|')) {
$names = explode('|', $name);
$cnt = 0;
foreach($names as $n) if(!$this->isSelectableFieldName($n, $level+1)) $cnt++;
return $cnt == 0;
} else if(strpos($name, '.')) {
list($name, $subname) = explode('.', $name);
if(!$this->isSelectableFieldName($subname, $level)) return false;
}
if(in_array($name, $this->nativeSorts)) $is = true;
else if(in_array($name, array('parent', 'template', 'template_label', 'has_parent', 'hasParent', 'children', 'numChildren', 'num_children', 'count'))) $is = true;
else if(!$level && in_array($name, array('include', 'status', 'check_access'))) $is = true;
else if(in_array($name, $this->fieldOptions)) $is = true;
if($name == 'pass' || $name == 'config') $is = false;
return $is;
}
protected function renderFullSearchForm() {
// Search options
$out = "\n\t";
$out .= "\n\t
" .
"\n\t" .
"\n\t" .
"\n\t
";
$out .= "\n\t" .
"\n\t" .
"\n\t" .
"\n\t
";
$out .= "\n\t" .
"\n\t" .
"\n\t" .
"\n\t";
// Advanced
$advCollapsed = true;
$out2 = "\n\t" .
"\n\t" .
"\n\t" .
"\n\t
";
$out2.= "\n\t" .
"\n\t" .
"\n\t" .
"\n\t
";
if($sort != 'relevance') {
$reverse = $this->input->whitelist('reverse');
$out2 .= "\n\t" .
"\n\t" .
"\n\t
";
if($reverse) $advCollapsed = false;
}
$display = $this->input->whitelist('display');
$out2.= "\n\t" .
"\n\t" .
"\n\t" .
"\n\t
";
if($display && $display != 'title,path') $advCollapsed = false;
$submit = $this->modules->get("InputfieldSubmit");
$submit->attr('name', 'submit');
$submit->attr('value', $this->_x('Search', 'submit')); // Search submit button for advanced search
$out .= "" . $submit->render() . "
";
$form = $this->modules->get("InputfieldForm");
$form->attr('id', 'ProcessPageSearchOptionsForm');
$form->method = 'get';
$form->action = './';
$field = $this->modules->get("InputfieldMarkup");
$field->label = $this->_("Search Options");
$field->value = $out;
$form->add($field);
$field = $this->modules->get("InputfieldMarkup");
if($advCollapsed) $field->collapsed = Inputfield::collapsedYes;
$field->label = $this->_("Advanced");
$field->value = $out2;
$form->add($field);
/* Remove temporarily
$field = $this->modules->get("InputfieldMarkup");
$field->id = 'ProcessPageSearchShortcuts';
$field->collapsed = true;
$field->label = $this->_("Shortcuts");
$field->value = $this->renderShortcuts();
*/
$form->add($field);
return $form->render();
}
protected function renderShortcuts() {
$out = '';
$links = array(
'Quick Links',
"All by creation date" => '?q=&submit=Search&display=title+path+created&sort=created&reverse=1' ,
"All by latest edit date" => '?q=&submit=Search&display=title+path+created&sort=modified&reverse=1',
"Users by creation date" => '?q=&template=user&submit=Search&operator=~%3D&display=name+email+created&sort=created&reverse=1',
'New pages by template',
);
foreach($this->templates as $template) {
// Quick links only for content with more than one page
// if($template->getNumPages() < 2) continue;
// Users get own quick link earlier, others are rather irrelevant
if($template->flags & Template::flagSystem) continue;
$links[$template->name] = "?q=&template={$template->name}&submit=Search&operator=~%3D&display=title+path+created&sort=created&reverse=1";
}
foreach($links as $label => $value) {
if(is_int($label)) {
$out .= "$value
";
} else {
$value .= "&show_options=1";
$value = htmlspecialchars($value);
$out .= "$label";
}
}
return $out;
}
protected function renderMatchesTable(PageArray $matches, array $display, $id = 'ProcessPageSearchResultsList') {
if(!count($display)) $display = array('path');
$out = '';
if(!count($matches)) return $out;
$table = $this->modules->get("MarkupAdminDataTable");
$table->setSortable(false);
$table->setEncodeEntities(false);
$header = $display;
$header[] = "";
$table->headerRow($header);
foreach($matches as $match) {
$match->setOutputFormatting(true);
$editUrl = "{$this->config->urls->admin}page/edit/?id={$match->id}";
$viewUrl = $match->url();
$row = array();
foreach($display as $name) {
$value = $match->get($name);
if(is_object($value)) {
if($value instanceof Page) $value = $value->name;
}
$value = strip_tags($value);
if($name == 'created' || $name == 'modified') $value = date(DATE_ISO8601, $value);
$row[] = "$value";
}
$row[] = $match->editable() ? "" . $this->_('edit') . "" : ' ';
$table->row($row);
}
if($matches->getTotal() > count($matches)) {
$pager = $this->wire('modules')->get('MarkupPagerNav');
if($this->input->urlSegment1 == 'for') $pager->setBaseUrl($this->page->url . "for/");
$pager = $pager->render($matches);
} else {
$pager = '';
}
$out = $pager . $table->render() . $pager;
return $out;
}
/**
* Find other types of ProcessWire assets that may be useful in search
*
* Applicable to adminSearchMode only.
*
* @param $q Text to find
* @return array Array of matches
*
*/
protected function findOtherTypes($q) {
$language = $this->wire('user')->language;
$language = $language && $language->id && !$language->isDefault() ? $language->id : '';
$results = array();
foreach($this->wire('fields') as $field) {
$name = $field->name;
$label = $field->{"label$language"};
if(stripos($name . $label, $q) !== false) $results[] = array(
'id' => $field->id,
'template_label' => str_replace('Fieldtype', '', $field->type),
'title' => $field->name,
'editUrl' => $this->wire('config')->urls->admin . "setup/field/edit?id=$field->id",
'type' => $this->_x('Fields', 'match-type')
);
}
foreach($this->wire('templates') as $template) {
$name = $template->name;
$label = $template->{"label$language"};
if(stripos($name . $label, $q) !== false) $results[] = array(
'id' => $template->id,
'template_label' => $template->name,
'title' => $label ? $label : $template->name,
'editUrl' => $this->wire('config')->urls->admin . "setup/template/edit?id=$template->id",
'type' => $this->_x('Templates', 'match-type')
);
}
foreach($this->wire('modules') as $module) {
//if($module instanceof ModulePlaceholder) continue;
$name = $module->className();
$info = $this->wire('modules')->getModuleInfo($module);
$title = $module instanceof ModulePlaceholder ? $name : $info['title'];
if(stripos($name . $title, $q) !== false) $results[] = array(
'id' => $module->id,
'template_label' => $name,
'title' => $title,
'editUrl' => $this->wire('config')->urls->admin . "module/edit?name=$name",
'type' => $this->_x('Modules', 'match-type')
);
}
return $results;
}
/**
* Render the provided matches as a JSON string for AJAX use
*
*/
protected function renderMatchesAjax(PageArray $matches, array $display, $selector) {
$a = array(
'selector' => $selector,
'total' => $matches->getTotal(),
'limit' => $matches->getLimit(),
'start' => $matches->getStart(),
'matches' => array(),
);
// determine which template label we'll be asking for (for multi-language support)
$templateLabel = 'label';
if(wire('languages')) {
$language = $this->user->language;
if($language && !$language->isDefault()) $templateLabel = "label$language";
}
if($this->adminSearchMode && $this->user->isSuperuser()) {
// enable search to include users when adminSearchMode and superuser
$a['matches'] = $this->findOtherTypes($this->input->get('admin_search'));
$users = $this->wire('users')->find("name%=" . $this->wire('sanitizer')->pageName($this->input->get('admin_search')));
if(count($users)) $matches->prepend($users);
}
foreach($matches as $page) {
$p = array(
'id' => $page->id,
'parent_id' => $page->parent_id,
'template' => $page->template->name,
'path' => $page->path,
'name' => $page->name,
);
if($this->adminSearchMode) {
// don't include non-editable pages in admin search mode
if(!$page->editable()) {
$a['total']--;
continue;
}
// include the type of match and URL to edit, when in adminSearchMode
$p['type'] = $this->_x('Pages', 'match-type');
$p['editUrl'] = $page->editable() ? $this->config->urls->admin . 'page/edit/?id=' . $page->id : '';
}
foreach($display as $key) {
if($key == 'template_label') {
$p['template_label'] = $page->template->$templateLabel ? $page->template->$templateLabel : $page->template->label;
if(empty($p['template_label'])) $p['template_label'] = $page->template->name;
continue;
}
$value = $page->get($key);
if(empty($value) && $this->adminSearchMode) {
if($key == 'title') $value = $page->name; // prevent empty title
}
if(is_object($value)) $value = $this->setupObjectMatch($value);
if(is_array($value)) $value = $this->setupArrayMatch($value);
$p[$key] = $value;
}
$a['matches'][] = $p;
}
return json_encode($a);
}
/**
* Convert object to an array where possible, otherwise convert to a string
*
* For use by renderMatchesAjax
*
*/
protected function setupObjectMatch($o) {
if($o instanceof Page) {
return array(
'id' => $o->id,
'parent_id' => $o->parent_id,
'template' => $o->template->name,
'name' => $o->name,
'path' => $o->path,
'title' => $o->title
);
}
if($o instanceof WireData || $o instanceof WireArray) return $o->getArray();
return (string) $o;
}
/**
* Filter an array converting any indexes containing objects to arrays or strings
*
* For use by renderMatchesAjax
*
*/
protected function setupArrayMatch(array $a) {
foreach($a as $key => $value) {
if(is_object($value)) $a[$key] = $this->setupObjectMatch($value);
else if(is_array($value)) $a[$key] = $this->setupArrayMatch($value);
}
return $a;
}
public function renderSearchForm($placeholder = '') {
$q = substr($this->input->get->q, 0, 128);
$q = $this->wire('sanitizer')->entities($q);
$adminURL = $this->wire('config')->urls->admin;
if($placeholder) {
$placeholder = $this->wire('sanitizer')->entities1($placeholder);
$placeholder = " placeholder='$placeholder'";
} else {
$placeholder = '';
}
$out = "\n";
return $out;
}
static public function getModuleConfigInputfields(array $data) {
$inputfields = new InputfieldWrapper();
$inputfield = Wire::getFuel('modules')->get("InputfieldText");
$inputfield->attr('name', 'searchFields');
if(!isset($data['searchFields'])) $data['searchFields'] = 'title body';
if(is_array($data['searchFields'])) $data['searchFields'] = implode(' ', $data['searchFields']);
$inputfield->attr('value', $data['searchFields']);
$inputfield->label = __("Default fields to search");
$description = __("Enter the names for one or more text-based fields that you want to search, separating each by a space."); // Default fields description
$inputfield->description = $description;
$inputfields->append($inputfield);
$inputfield = Wire::getFuel('modules')->get("InputfieldText");
$inputfield->attr('name', 'searchFields2');
if(!isset($data['searchFields2'])) $data['searchFields2'] = 'title';
if(is_array($data['searchFields2'])) $data['searchFields2'] = implode(' ', $data['searchFields2']);
$inputfield->attr('value', $data['searchFields2']);
$inputfield->label = __("Field(s) to search in admin search (ajax) mode");
$inputfield->description = $description;
$inputfield->notes = __("We recommend limiting this to 1 or 2 fields at the most since results populate a live autocomplete field. Typically you would just search the title."); // Fields to search description
$inputfields->append($inputfield);
$inputfield = Wire::getFuel('modules')->get("InputfieldText");
$inputfield->attr('name', 'displayField');
$inputfield->attr('value', isset($data['displayField']) ? $data['displayField'] : 'name');
$inputfield->label = __("Default field name(s) to display in search results");
$inputfield->description = __("If specifying more than one field, separate each with a space.");
$inputfields->append($inputfield);
$inputfield = Wire::getFuel('modules')->get("InputfieldSelect");
$inputfield->attr('name', 'operator');
$inputfield->attr('value', isset($data['operator']) ? $data['operator'] : self::defaultOperator);
$inputfield->label = __("Default search operator");
foreach(self::getOperators() as $operator => $label) {
$inputfield->addOption($operator, "$operator $label");
}
$inputfields->append($inputfield);
return $inputfields;
}
}