artabro/wire/modules/Process/ProcessPageSearch/ProcessPageSearchLive.php

1197 lines
34 KiB
PHP
Raw Normal View History

2024-08-27 11:35:37 +02:00
<?php namespace ProcessWire;
/**
* ProcessPageSearch: Live Search (for PW admin)
*
* @method renderList(array $items, $prefix = 'pw-search', $class = 'list')
* @method renderItem(array $item, $prefix = 'pw-search', $class = 'list')
* @method string|array execute($getJSON = true)
*
* @todo support searching repeaters
*
*/
class ProcessPageSearchLive extends Wire {
/**
* Reference to ProcessPageSearch, if available
*
* @var Process|ProcessPageSearch
*
*/
protected $process;
/**
* Properties to skip in selectors
*
* @var array
*
*/
protected $skipProperties = array(
'include',
'check_access',
'checkaccess',
);
/**
* Template for live search settings
*
* @var array
*
*/
protected $liveSearchDefaults = array(
'type' => '', // type of search, if not pages, i.e. "templates", "fields", "modules", "comments", etc.
'property' => '', // property to search for within type, or blank if no specific property
'operator' => '%=',
'q' => '', // query text to find
'selectors' => array(),
'template' => null,
'multilang' => true,
'language' => '', // language name
'edit' => true,
'start' => 0,
'limit' => 15,
'verbose' => false,
'debug' => false,
'help' => false,
);
/**
* Template for individual live search result items
*
* @var array
*
*/
protected $itemTemplate = array(
'id' => 0,
'url' => '', // required
'name' => '',
'title' => '', // required
'subtitle' => '',
'summary' => '',
'icon' => '',
'group' => '',
'status' => 0,
'modified' => 0,
);
/**
* Allowed operators
*
* @var array
*
*/
protected $allowOperators = array(
'=', '==', '!=', '*=', '~=', '%=', '^=', '$=', '<=', '>=', '<', '>'
);
/**
* Operator to use for single-word matches (if not overridden)
*
* @var string
*
*/
protected $singleWordOperator = '%=';
/**
* Operator to use for multi-word matches (if not overridden)
*
* @var string
*
*/
protected $multiWordOperator = '~=';
/**
* Default fields to search for pages
*
* @var array
*
*/
protected $defaultPageSearchFields = array('title');
/**
* Are we currently in “view all” mode?
*
* @var bool
*
*/
protected $isViewAll = false;
/**
* Order to render results in, by search type
*
* @var array
*
*/
protected $searchTypesOrder = array();
/**
* Search types that are specifically excluded
*
* @var array
*
*/
protected $noSearchTypes = array();
/**
* PaginatedArray to use for pagination, when applicable for “view all” mode
*
* @var null|PaginatedArray
*
*/
protected $pagination = null;
/**
* Shared translation labels, defined in constructor
*
* @var array
*
*/
protected $labels = array();
/**
* Construct
*
* @param Process|ProcessPageSearch $process
* @param array $liveSearch
*
*/
public function __construct(Process $process = null, array $liveSearch = array()) {
if($process) {
$process->wire($this);
if($process instanceof ProcessPageSearch) $this->process = $process;
$searchFields = $this->wire()->config->ajax ? $process->searchFields2 : $process->searchFields;
$a = explode(' ', $searchFields);
if(count($a)) $this->defaultPageSearchFields = $a;
}
if(!empty($liveSearch)) {
$this->liveSearchDefaults = array_merge($this->liveSearchDefaults, $liveSearch);
}
$findOperators = Selectors::getOperators(array(
'compareType' => Selector::compareTypeFind,
'getIndexType' => 'none',
'getValueType' => 'operator',
));
$this->allowOperators = array_unique(array_merge($this->allowOperators, $findOperators));
$this->labels = array(
'missing-query' => $this->_('No search specified'),
'pages' => $this->_('Pages'),
'trash' => $this->_('Trash'),
'modules' => $this->_('Modules'),
'view-all' => $this->_('View All'),
'search-results' => $this->_('Search Results'),
);
parent::__construct();
}
/**
* Set order of search types
*
* @param array $types Names of types, in order
*
*/
public function setSearchTypesOrder(array $types) {
$this->searchTypesOrder = $types;
}
/**
* Set types that should be excluded unless specifically asked for
*
* @param array $types Names of types to exclude
*
*/
public function setNoSearchTypes(array $types) {
$this->noSearchTypes = $types;
}
/**
* Set default operators to use for searches (if query does not specify operator)
*
* @param string $singleWordOperator
* @param string $multiWordOperator
*
*/
public function setDefaultOperators($singleWordOperator, $multiWordOperator = '') {
$this->singleWordOperator = $singleWordOperator;
$this->multiWordOperator = empty($multiWordOperator) ? $singleWordOperator : $multiWordOperator;
}
/**
* Initialize live search
*
* @param array $presets Additional info to populate in liveSearchInfo
* @return array Current liveSearchInfo
*
*/
protected function init(array $presets = array()) {
$input = $this->wire()->input;
$sanitizer = $this->wire()->sanitizer;
$fields = $this->wire()->fields;
$templates = $this->wire()->templates;
$user = $this->wire()->user;
$languages = $this->wire()->languages;
$adminTheme = $this->wire()->adminTheme;
$type = isset($presets['type']) ? $presets['type'] : '';
$language = isset($presets['language']) ? $presets['language'] : '';
$property = isset($presets['property']) ? $presets['property'] : '';
$operator = isset($presets['operator']) ? $presets['operator'] : '';
$template = isset($presets['template']) ? $presets['template'] : '';
$limit = isset($presets['limit']) ? (int) $presets['limit'] : $this->liveSearchDefaults['limit'];
$start = isset($presets['start']) ? (int) $presets['start'] : ($input->pageNum() - 1) * $limit;
$selectors = array();
$replaceOperator = '';
$opHolders = array('<=' => '~@LT=', '>=' => '~@GT=', '<' => '~@LT', '>' => '~@GT'); // operator placeholders
$q = empty($presets['q']) ? $input->get('q') : $presets['q'];
if(empty($q)) $q = $input->get('admin_search'); // legacy name
if(strpos($q, '~@') !== false) $q = str_replace('~@', '', $q); // disallow placeholder prefix
if(empty($operator)) $q = str_replace(array_keys($opHolders), array_values($opHolders), $q);
$q = $sanitizer->text($q, array('reduceSpace' => true));
if($user->isSuperuser() && strpos($q, 'DEBUG') !== false) {
$q = str_replace('DEBUG', '', $q);
$presets['debug'] = true;
}
if(empty($q)) {
// if no query, we've got nothing to do
return $this->liveSearchDefaults;
}
if(empty($operator)) {
// operator may be bundled into query: $q
if(strpos($q, '~@') !== false) {
foreach($opHolders as $op => $placeholder) { // <=, >=, <, >
if(strpos($q, $placeholder) === false) continue;
$replaceOperator = $placeholder;
$operator = $op;
break;
}
} else if(strpos($q, '==') !== false) {
// forced equals operator
$replaceOperator = '==';
$operator = '=';
} else if(strpos($q, '=') !== false) {
// regular equals or other w/equals
$replaceOperator = '=';
$opChars = Selectors::getOperatorChars();
if(preg_match('/([' . preg_quote(implode('', $opChars)) . ']{1,3}=)/', $q, $matches)) {
if(in_array($matches[1], $this->allowOperators)) {
$operator = $matches[1];
$replaceOperator = $operator;
} else {
$q = str_replace($opChars, ' ', $q);
}
} else {
// regular equals, use default operator
}
}
if($replaceOperator) {
$q = str_replace($replaceOperator, ':', $q);
}
}
if(empty($operator) || !in_array($operator, $this->allowOperators)) {
$operator = strpos($q, ' ') ? $this->multiWordOperator : $this->singleWordOperator;
}
// check if type and property may be part of query: $q
if(empty($type) && empty($property) && strpos($q, ':')) {
// Search specifies a specific type "type:text", i.e. "users:ryan"
list($type, $q) = explode(':', $q, 2);
// live search type: pages, users, modules, fields, templates, comments, etc.
$type = $sanitizer->name($type);
if(strpos($type, '.') !== false) {
// live search type includes a property, i.e. "pages.body", "users.first_name", etc.
list($type, $property) = explode('.', $type, 2);
}
if($type === 'pages') {
// ok
} else if($type) {
// check if type refers to a template name or language
$template = true;
$language = true;
} else {
// search all types
}
} else if($type) {
$template = true;
$language = true;
}
if($language === true) {
// check if type refers to a language
$language = $languages ? $languages->get($type) : null;
if($language && $language->id) {
$language = $language->name;
$template = null;
$type = '';
} else {
$language = '';
}
}
if($template === true) {
// check if type refers to template name or language
$template = $templates->get($type);
}
if($template instanceof Template) {
// does search type match the name of a template?
$selectors[] = "template=$template->name";
$type = '';
// $type = 'pages';
}
$type = $sanitizer->name($type);
$property = $sanitizer->fieldName($property);
$q = trim($q);
$value = $sanitizer->selectorValue($q);
$lp = strtolower($property);
if($property && ($fields->isNative($property) || $fields->get($property)) && !in_array($lp, $this->skipProperties)) {
// we recognize this property as searchable, so add it to the selector
if($lp == 'status' && !$user->isSuperuser() && $value > Page::statusHidden) $value = Page::statusHidden;
$selectors[] = $property . $operator . $value;
} else {
// we did not recognize the property, so use field(s) defined in module instead
$selectors[] = implode('|', $this->defaultPageSearchFields) . $operator . $value;
}
$help = strtolower($q) === 'help';
if(!$help && $adminTheme && $q === $adminTheme->getLabel('search-help')) $help = true;
$liveSearch = array_merge($this->liveSearchDefaults, $presets, array(
'type' => $type,
'property' => $property,
'operator' => $operator,
'q' => $q,
'selectors' => $selectors,
'template' => $template,
'multilang' => $languages ? true : false,
'language' => $language,
'start' => $start,
'limit' => $limit,
'help' => $help,
));
if($this->isViewAll) {
// variables for pagination
$input->whitelist('type', $type);
$input->whitelist('property', $property);
$input->whitelist('operator', $operator);
$input->whitelist('q', $q);
if(!empty($liveSearch['language'])) $input->whitelist('language', $liveSearch['language']);
}
return $liveSearch;
}
/**
* Execute live search and return JSON result
*
* @param bool $getJSON Get results as JSON string? Specify false to get array instead.
* @return string|array
*
*/
public function ___execute($getJSON = true) {
$input = $this->wire()->input;
$liveSearch = $this->init();
if((int) $input->get('version') > 1) {
// version 2+ keep results in native format, for future use
$items = $this->find($liveSearch);
} else {
// version 1 is currently used by PW admin themes
$items = $this->convertItemsFormat($this->find($liveSearch));
}
$result = array(
'matches' => &$items
);
return $getJSON ? json_encode($result) : $items;
}
/**
* Render output for landing page to view all items of a particular type
*
* Expects these GET vars to be present:
* - type
* - operator
* - property
* - q
*
* @return string
* @throws WireException
*
*/
public function executeViewAll() {
$input = $this->wire()->input;
$this->isViewAll = true;
$type = $input->get->pageName('type');
$operator = $input->get('operator');
$property = $input->get->fieldName('property');
$language = $input->get->pageName('language');
$q = $input->get->text('q');
$this->pagination = new PaginatedArray();
$this->wire($this->pagination);
if(empty($q)) {
$this->error($this->labels['missing-query']);
return '';
}
if(false && ($type == 'pages' || $type == 'trash')) {
// let Lister handle it
$results = array();
} else {
$liveSearch = $this->init(array(
'type' => $type,
'property' => $property,
'operator' => $operator,
'q' => $q,
'limit' => $this->liveSearchDefaults['limit'],
'verbose' => true,
'language' => $language,
));
$results = $this->find($liveSearch);
}
if($this->process) {
if($type) {
$this->process->headline($this->pagination->getPaginationString(array(
'label' => $this->labels['search-results'] . " - " . ucfirst($type),
'count' => count($results)
)));
} else {
$this->process->headline($this->labels['search-results']);
}
}
$out = $this->renderList($results);
return $out;
}
/**
* Perform find of types, pages, modules
*
* Result format that this find method expects from modules it calls the search() method from:
*
* $result = array(
* 'title' => 'Title of these items, used as the group label except where overridden item "group" property',
* 'url' => 'URL to view all items', // if omitted, one will be provided automatically
* 'total' => 999, // non-paginated total quantity (can be omitted if pagination not supported)
* 'items' => [
* [
* // required properties
* 'title' => 'Title of item',
* 'url' => 'URL to view or edit the item',
* // optional properties:
* 'id' => 0,
* 'name' => 'Name of item',
* 'icon' => 'Optional icon name to represent the item, i.e. "gear" or "fa-gear"',
* 'group' => 'Optionally group with other items having this group name, overrides $result[title]',
* 'status' => int, // if item is a Page, status of page using Page::status* constants
* 'summary' => 'Summary or description of item or excerpt of text that matched', // (recommended)
* 'subtitle' => 'Secondary title of item', // (recommended)
* 'modified' => int, // last modified date of item
* ],
* [ ... ], [ ... ], etc.
* )
* );
*
* @param array $liveSearch
* @return array Array of matches
*
*/
protected function find(array &$liveSearch) {
$user = $this->wire()->user;
$modules = $this->wire()->modules;
$languages = $this->wire()->languages;
$items = array();
$userLanguage = null;
$q = $liveSearch['q'];
$type = $liveSearch['type'];
$foundTypes = array();
$modulesInfo = array();
$help = $liveSearch['help'];
if($languages && $liveSearch['language']) {
// change current user to have requested language, temporarily
$language = $languages->get($liveSearch['language']);
if($language && $language->id) {
$userLanguage = $user->language;
$user->language = $language;
}
}
if($type != 'pages' && $type != 'trash') {
$modulesInfo = $modules->getModuleInfo('*', array('verbose' => true));
}
foreach($modulesInfo as $info) {
if(empty($info['searchable'])) continue;
$name = $info['name'];
$thisType = $info['searchable'];
if(!$this->useType($thisType, $type)) continue;
if($type && $this->isViewAll && $type != $thisType) continue;
if($type && stripos($thisType, $type) === false) continue;
if(!empty($liveSearch['template']) && !empty($liveSearch['property'])) continue;
if(!$user->isSuperuser() && !$modules->hasPermission($name, $user)) continue;
$foundTypes[] = $thisType;
$module = null;
$result = array();
try {
/** @var SearchableModule $module */
$module = $modules->getModule($name, array('noInit' => true));
if(!$module) continue;
$result = $module->search($q, $liveSearch); // see method phpdoc for $result format
} catch(\Exception $e) {
// ok
}
if(!$module || (empty($result['items']) && empty($liveSearch['help']))) continue;
if(empty($result['total'])) $result['total'] = count($result['items']);
if(!in_array($thisType, $this->searchTypesOrder)) $this->searchTypesOrder[] = $thisType;
$order = array_search($thisType, $this->searchTypesOrder);
$order = $order * 100;
$title = empty($result['title']) ? "$info[title]" : "$result[title]";
$n = $liveSearch['start'];
$item = null;
if($help) {
foreach($result['items'] as $key => $item) {
if($item['name'] != 'help') unset($result['items'][$key]);
}
$result['items'] = array_merge($this->makeHelpItems($result, $thisType), $result['items']);
}
foreach($result['items'] as $item) {
$n++;
$item = array_merge($this->itemTemplate, $item);
$item['group'] = empty($item['group']) ? "$title" : "$item[group]";
if(empty($item['group'])) $item['group'] = $title;
$item['n'] = "$n/$result[total]";
$items[$order] = $item;
$order++;
}
//if($n && $n < $result['total'] && !$this->isViewAll && !$help) {
if($n && $n < $result['total'] && !$help) {
$url = isset($result['url']) ? $result['url'] : '';
$items[$order] = $this->makeViewAllItem($liveSearch, $thisType, $item['group'], $result['total'], $url);
}
if($this->isViewAll && $this->pagination && $type && !$help) {
$this->pagination->setTotal($result['total']);
$this->pagination->setLimit($liveSearch['limit']);
$this->pagination->setStart($liveSearch['start']);
}
}
if($type && !$help && !count($foundTypes) && !in_array($type, array('pages', 'trash', 'modules'))) {
if(empty($liveSearch['template'])) {
// if no types matched, and its going to skip pages, assume type is a property, and do a pages search
$liveSearch = $this->init(array(
'q' => $liveSearch['q'],
'type' => 'pages',
'property' => $type,
'operator' => $liveSearch['operator']
));
$type = 'pages';
}
}
if(empty($type) || $type === 'pages' || $type === 'trash' || $liveSearch['template']) {
// include pages in the search results
if(!in_array('pages', $this->searchTypesOrder)) $this->searchTypesOrder[] = 'pages';
$order = array_search('pages', $this->searchTypesOrder) * 100;
foreach($this->findPages($liveSearch) as $item) {
$items[$order++] = $item;
}
}
// use built-in modules search when appropriate
if($this->useType('modules', $type) && $this->wire()->user->isSuperuser()) {
if(!in_array('modules', $this->searchTypesOrder)) $this->searchTypesOrder[] = 'modules';
$order = array_search('modules', $this->searchTypesOrder) * 100;
foreach($this->findModules($liveSearch, $modulesInfo) as $item) {
$items[$order++] = $item;
}
}
// add a debug item if requested to
if(!empty($liveSearch['debug'])) {
array_unshift($items, $this->makeDebugItem($liveSearch));
}
if($userLanguage) {
// restore original language to user
$user->language = $userLanguage;
}
ksort($items);
if($help) $items = array_merge($this->makeHelpItems(array(), 'help'), $items);
return $items;
}
/**
* Find pages for live search
*
* @param array $liveSearch
* @return array
*
*/
protected function findPages(array &$liveSearch) {
$pages = $this->wire()->pages;
$config = $this->wire()->config;
$user = $this->wire()->user;
$superuser = $user->isSuperuser();
if(!empty($liveSearch['help'])) {
$result = array('title' => 'pages', 'items' => array(), 'properties' => array('name', 'title'));
if($this->wire()->fields->get('body')) $result['properties'][] = 'body';
$result['properties'][] = $this->_('or any field name');
return $this->makeHelpItems($result, 'pages');
}
// a $pages->find() search will be included in the live search
$selectors = &$liveSearch['selectors'];
$selectors[] = "start=$liveSearch[start], limit=$liveSearch[limit]";
if($this->process) {
$repeaterID = $this->process->getRepeatersPageID();
if($repeaterID) $selectors[] = "has_parent!=$repeaterID";
}
if($superuser) {
// superuser only
$selectors[] = "include=all";
} else if($user->hasPermission('page-edit')) {
// admin search mode and user has some kind of page-edit permission
$selectors[] = "include=unpublished";
// $selectors[] = "template=$editableTemplates";
// $selectors[] = "status<" . Page::statusTrash;
} else {
// only show regular, non-hidden, non-unpublished pages
}
$selector = implode(', ', $selectors);
if($this->process) $selector = $this->process->findReady($selector);
$titles = array();
$items = array();
$matches = array('pages' => array(), 'trash' => array());
try {
if($this->useType('pages', $liveSearch['type'])) {
$selector .= ', templates_id!=' . implode('|', $config->userTemplateIDs); // users are searched separately
$items['pages'] = $pages->find("$selector, status<" . Page::statusTrash);
}
} catch(\Exception $e) {
}
try {
if($superuser && $this->useType('trash', $liveSearch['type'])) {
$items['trash'] = $pages->find("$selector, status>=" . Page::statusTrash);
}
} catch(\Exception $e) {
}
foreach($items as $type => $pageItems) {
$n = $liveSearch['start'];
$total = $pageItems->getTotal();
$item = array();
foreach($pageItems as $page) {
/** @var Page $page */
if(!$superuser && $page->isUnpublished() && !$page->editable()) continue;
$isAdmin = $page->template == 'admin';
$title = (string) $page->get('title|name');
$item = array(
'id' => $page->id,
'name' => $page->name,
'title' => $title,
'subtitle' => $page->template->name,
'summary' => $page->path,
'url' => !$isAdmin && $page->editable() ? $page->editUrl(array('language' => true)) : $page->url(),
'icon' => $page->getIcon(),
'group' => '',
'n' => (++$n) . '/' . $total,
'modified' => $page->modified,
'status' => $page->status,
);
if(!isset($titles[$title])) $titles[$title] = 0;
$titles[$title]++;
$item['group'] = $this->labels[$type];
$matches[$type][] = $item;
}
if(!empty($item) && $total > count($matches[$type])) {
$matches[$type][] = $this->makeViewAllItem($liveSearch, $type, $item['group'], $total, '');
}
}
// merge all the matches together
if(empty($matches['trash'])) {
$matches = $matches['pages'];
} else {
$matches = array_merge($matches['pages'], $matches['trash']);
}
// if there any colliding titles, add modified date to the subtitle
foreach($titles as $title => $qty) {
if($qty < 2) continue;
foreach($matches as $key => $item) {
if($item['title'] !== $title) continue;
$matches[$key]['subtitle'] .= " (" . wireRelativeTimeStr($item['modified'], true) . ")";
}
}
return $matches;
}
/**
* Allow this search type?
*
* @param string $type Type to check
* @param string $requestType Type specifically requested by user
* @return bool
*
*/
protected function useType($type, $requestType = '') {
if($requestType) return $type === $requestType;
return !in_array($type, $this->noSearchTypes);
}
/**
* Find modules matching query
*
* @param array $liveSearch
* @param array $modulesInfo
* @return array
*
*/
protected function findModules(array &$liveSearch, array &$modulesInfo) {
$modules = $this->wire()->modules;
$adminUrl = $this->wire()->config->urls->admin;
$q = $liveSearch['q'];
$groupLabel = $this->labels['modules'];
$items = array();
$forceMatch = false;
if(!empty($liveSearch['help'])) {
$info = $modules->getModuleInfoVerbose('ProcessPageSearch');
$properties = array();
foreach(array_keys($info) as $property) {
$value = $info[$property];
if(!is_array($value)) $properties[$property] = $property;
}
$exclude = array('id', 'file', 'versionStr', 'core');
foreach($exclude as $key) unset($properties[$key]);
$result = array(
'title' => 'Modules',
'items' => array(),
'properties' => $properties
);
$items = $this->makeHelpItems($result, 'modules');
return $items;
}
if($liveSearch['type'] === 'modules' && !empty($liveSearch['property'])) {
// searching for custom module property
$forceMatch = true;
$infos = $modules->findByInfo(
$liveSearch['property'] . $liveSearch['operator'] .
$this->wire()->sanitizer->selectorValue($q), 2
);
} else {
// text-matching for all modules
$infos = &$modulesInfo;
}
foreach($infos as $info) {
$id = isset($info['id']) ? $info['id'] : 0;
$name = $info['name'];
$title = $info['title'];
$summary = isset($info['summary']) ? $info['summary'] : '';
if(!$forceMatch) {
$searchText = "$name $title $summary";
if(stripos($searchText, $q) === false) continue;
}
$item = array(
'id' => $id,
'name' => $name,
'title' => $title,
'subtitle' => $name,
'summary' => $summary,
'url' => $adminUrl . "module/edit?name=$name",
'group' => $groupLabel,
);
$item = array_merge($this->itemTemplate, $item);
$items[] = $item;
}
$total = count($items);
$n = 0;
foreach($items as $key => $item) {
$n++;
$items[$key]['n'] = "$n/$total";
}
return $items;
}
/**
* Convert items from native live search format (v2) to v1 format
*
* v1 format is used by ProcessWire admin themes.
*
* @param array $items
* @return array
*
*/
protected function convertItemsFormat(array $items) {
$converted = array();
$sanitizer = $this->wire()->sanitizer;
foreach($items as $item) {
$a = array(
'id' => $item['id'],
'name' => (string) $item['name'],
'title' => (string) $item['title'],
'template_label' => (string) $item['subtitle'],
'tip' => (string) $item['summary'],
'editUrl' => (string) $item['url'],
'type' => (string) $sanitizer->entities($item['group']),
'icon' => isset($item['icon']) ? $item['icon'] : '',
);
if(!empty($item['status'])) {
if($item['status'] & Page::statusUnpublished) $a['unpublished'] = true;
if($item['status'] & Page::statusHidden) $a['hidden'] = true;
if($item['status'] & Page::statusLocked) $a['locked'] = true;
}
$converted[] = $a;
}
return $converted;
}
/**
* Make a search result item that displays debugging info
*
* @param array $liveSearch
* @return array
*
*/
protected function makeDebugItem($liveSearch) {
$liveSearch['user_language'] = $this->wire()->user->language->name;
$summary = print_r($liveSearch, true);
return array_merge($this->itemTemplate, array(
'id' => 0,
'name' => 'debug',
'title' => implode(', ', $liveSearch['selectors']),
'subtitle' => $liveSearch['q'],
'summary' => $summary,
'url' => '#',
'group' => 'Debug',
));
}
/**
* Make a search result item that displays property info
*
* @param array $result Result array returned by a SearchableModule::search() method
* @param string $type
* @return array
*
*/
protected function makeHelpItems(array $result, $type) {
$sanitizer = $this->wire()->sanitizer;
$items = array();
$helloLabel = $this->_('test');
$usage1desc = $sanitizer->unentities($this->_('Searches %1$s for: %2$s'));
$usage2desc = $sanitizer->unentities($this->_('Searches “%1$s” property of %2$s for: %3$s'));
if($type === 'help') {
$operators = ProcessPageSearch::getOperators();
$summary =
$this->_('Examples use the “=” equals operator.') . " \n" .
$this->_('In some cases you can also use these:') . "\n";
foreach($operators as $op => $label) {
$summary .= "$op " . rtrim($label, '*') . "\n";
}
$items[] = array(
'title' => $this->_('operators:'),
'subtitle' => implode(', ', array_keys($operators)),
'summary' => $summary,
'group' => 'help',
'url' => 'https://processwire.com/api/selectors/#operators'
);
if($this->wire()->user->isSuperuser() && $this->process) {
$items[] = array(
'title' => $this->_('configure'),
'subtitle' => $this->_('Click here to configure search settings'),
'url' => $this->wire()->modules->getModuleEditUrl('ProcessPageSearch'),
'group' => 'help',
);
}
foreach($items as $key => $item) $items[$key] = array_merge($this->itemTemplate, $item);
return $items;
}
// include any items from result that had the name "help"
foreach($result['items'] as $item) {
if($item['name'] == 'help') $items[] = $item;
}
$items[] = array(
'title' => "$type=$helloLabel",
'subtitle' => sprintf($usage1desc, $type, $helloLabel),
);
if(!empty($result['properties'])) {
if($type == 'pages' || $type == 'modules') {
$property = 'title';
} else if($type == 'fields' || $type == 'templates') {
$property = 'label';
} else {
$property = reset($result['properties']);
}
$items[] = array(
'title' => "$type.$property=$helloLabel",
'subtitle' => sprintf($usage2desc, $property, $type, $helloLabel)
);
if($type === 'pages') {
$items[] = array(
'title' => "$property=$helloLabel",
'subtitle' => $this->_('Same as above (shorter syntax if no names collide)')
);
$templateName = 'basic-page';
$items[] = array(
'title' => "$templateName=$helloLabel",
'subtitle' => sprintf($this->_('Limit results to template: %s'), $templateName),
);
$items[] = array(
'title' => "$templateName.$property=$helloLabel",
'subtitle' => sprintf($this->_('Limit results to %s field on template'), $property)
);
} else if($type === 'templates') {
$fieldName = 'images';
$items[] = array(
'title' => "templates.fields=$fieldName",
'subtitle' => sprintf($this->_('Find templates that have field: %s'), $fieldName)
);
} else if($type === 'fields') {
$items[] = array(
'title' => "fields.settings=ckeditor",
'subtitle' => $this->_('Find fields with “ckeditor” in settings'),
);
}
$properties = implode(', ', $result['properties']);
if(strlen($properties) > 50) {
$properties = $this->wire()->sanitizer->truncate($properties, 50) . ' ' . $this->_('(hover for more)');
}
$summary =
sprintf($this->_('The examples use the “%s” property.'), $property) . "\n" .
$this->_('You can also use any of these properties:') . "\n" .
implode("\n", $result['properties']);
$items[] = array(
'title' => $this->_('properties'),
'subtitle' => $properties,
'summary' => $summary
);
}
$group = sprintf($this->_('%s help'), $type);
foreach($items as $key => $item) {
$item['name'] = 'help';
$item['group'] = $group;
$items[$key] = array_merge($this->itemTemplate, $item);
}
return $items;
}
/**
* Make a search result item that displays a “view all” link
*
* @param array $liveSearch
* @param string $type
* @param string $group
* @param int $total
* @param string $url If module provides its own view-all URL
* @return array
*
*/
protected function makeViewAllItem(&$liveSearch, $type, $group, $total, $url = '') {
if(!empty($url)) {
// use provided url
} else if($type == 'pages' || $type == 'trash' || !empty($liveSearch['template'])) {
$url = $this->wire()->page->url();
$url .= "?q=" . urlencode($liveSearch['q']) . "&live=1";
if($type == 'trash') $url .= "&trash=1";
if(!empty($liveSearch['template'])) {
$url .= "&template=" . $liveSearch['template']->name;
}
if(!empty($liveSearch['property'])) {
$url .= "&field=" . urlencode($liveSearch['property']);
}
if(!empty($liveSearch['operator'])) {
$url .= "&operator=" . urlencode($liveSearch['operator']);
}
} else {
$url = $this->wire('page')->url() . 'live/' .
'?q=' . urlencode($liveSearch['q']) .
'&type=' . urlencode($type) .
'&property=' . urlencode($liveSearch['property']) .
'&operator=' . urlencode($liveSearch['operator']);
}
return array_merge($this->itemTemplate, array(
'id' => 0,
'name' => 'view-all',
'title' => $this->labels['view-all'],
'subtitle' => sprintf($this->_('%d items'), $total),
'summary' => '',
'url' => $url,
'group' => $group,
));
}
/**
* Render “view all” list
*
* @param array $items
* @param string $prefix For CSS classes, default is "pw-search"
* @param string $class Class name for list, default is "list" which translates to "pw-search-list"
* @return string HTML markup
*
*/
protected function ___renderList(array $items, $prefix = 'pw-search', $class = 'list') {
$pagination = $this->pagination->renderPager();
$group = '';
$groups = array();
$totals = array();
$counts = array();
$btn = $this->wire()->modules->get('InputfieldButton'); /** @var InputfieldButton $btn */
$btn->aclass = "$prefix-view-all";
foreach($items as $item) {
if($item['group'] != $group) {
$group = $item['group'];
$groups[$group] = '';
}
$counts[$group] = isset($counts[$group]) ? $counts[$group] + 1 : 1;
if(empty($totals[$group]) && isset($item['n'])) {
list(, $total) = explode('/', $item['n']);
$totals[$group] = (int) $total;
}
if($item['name'] === 'view-all') {
if($pagination) continue;
$btn->href = $item['url'];
$btn->value = "$item[title] > $group (" . $totals[$group] . ")";
$groups[$group] .= $btn->render();
} else {
$groups[$group] .= $this->renderItem($item, $prefix) . '<hr />';
}
}
$totalGroups = array();
foreach($groups as $group => $content) {
$total = empty($totals[$group]) ? $counts[$group] : (int) $totals[$group];
$totalGroups["$group ($total)"] = $content;
unset($groups[$group]);
}
/** @var JqueryWireTabs $wireTabs */
$wireTabs = $this->wire()->modules->get('JqueryWireTabs');
return
"<div class='pw-search-$class'>" .
$pagination .
$wireTabs->render($totalGroups) .
$pagination .
"</div>";
}
/**
* Render an item for the “view all” list
*
* @param array $item
* @param string $prefix For CSS classes, default is "pw-search"
* @param string $class Class name for item, default is "item" which translates to "pw-search-item"
* @return string HTML markup
*
*/
protected function ___renderItem(array $item, $prefix = 'pw-search', $class = 'item') {
$sanitizer = $this->wire()->sanitizer;
foreach(array('title', 'subtitle', 'summary', 'url') as $key) {
if(isset($item[$key])) {
$item[$key] = $sanitizer->entities($item[$key]);
} else {
$item[$key] = '';
}
}
$title = "<strong class='$prefix-title'>$item[title]</strong>";
$subtitle = empty($item['subtitle']) ? '' : "<br /><em class='$prefix-subtitle'>$item[subtitle]</em> ";
$summary = empty($item['summary']) ? '' : "<br /><span class='$prefix-summary'>$item[summary]</span> ";
return "\n\t<div class='$prefix-$class'><p><a href='$item[url]'>$title</a> $subtitle $summary</p></div>";
}
}