'', // 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 it’s 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) . '
$title $subtitle $summary