Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Page Auto Completion select widget** This Inputfield connects the jQuery UI Autocomplete widget with the ProcessWire ProcessPageSearch AJAX API.** ProcessWire 3.x, Copyright 2019 by Ryan Cramer* https://processwire.com** @property int $parent_id Limit results to this parent, or if combined with findPagesSelector, the search is performed as $pages->get($parent_id)->find() rather than $pages->find().* @property int $template_id Limit results to pages using this template.* @property array $template_ids Limit results to pages using this templates (alternate to the single template_id).* @property string $labelFieldName Field to display in the results. (default=title)* @property string $labelFieldFormat Format string to display in the results, overrides labelFieldName when used (default=blank).* @property string $searchFields Field(s) to search for text. Separate multiple by a space. (default=title)* @property string $operator Selector operator to use in performing the search (default: %=)* @property string $findPagesSelector Optional selector to use for finding pages (default=blank)* @property int $maxSelectedItems Maximum number of items that may be selected (0=unlimited, default).* @property bool $useList Whether or not to use a separate selected list. If false specified, selected item will be populated directly to the input. (default=true)* @property bool $allowAnyValue Allow any value to stay in the input, even if not selectable? (default=false)* @property string $disableChars Autocomplete won't be triggered if input contains any of the characters in this string. (default=blank)* @property bool $useAndWords When true, each word will be isolated to a separate searchFields=searchValue, which enables it to duplicate ~= operator behavior while using a %= operator (default=false).* @property int $lang_id Force use of this language for results** @method string renderList()* @method string renderListItem($label, $value, $class = '')**/class InputfieldPageAutocomplete extends Inputfield implements InputfieldHasArrayValue, InputfieldHasSortableValue {public static function getModuleInfo() {return array('title' => __('Page Auto Complete', __FILE__), // Module Title'summary' => __('Multiple Page selection using auto completion and sorting capability. Intended for use as an input field for Page reference fields.', __FILE__), // Module Summary'version' => 112,);}/*** Initialize variables used for the autocompletion**/public function init() {parent::init();// limit results to this parent, or if combined with a 'findPagesSelector',// the search is performed as $pages->get($parent_id)->find() rather than $pages->find()$this->set('parent_id', 0);// limit results to pages using this template$this->set('template_id', 0);$this->set('template_ids', array());// field to display in the results$this->set('labelFieldName', 'title');// format string to display in the results (instead of labelFieldName, when used)$this->set('labelFieldFormat', '');// field(s) to search for text, separate multiple by a space$this->set('searchFields', 'title');// operator to use in performing the search$this->set('operator', '%=');// optional selector to use for all other properties$this->set('findPagesSelector', '');// maximum number of items that may be selected$this->set('maxSelectedItems', 0);// whether or not to use the selection list// if not used, selected value will be populated directly to input$this->set('useList', true);// allow a non-selectable value to stay in the autocomplete input?// useful for situations where autocomplete might be used for collection of other values// like when used with ProcessPageEditLink$this->set('allowAnyValue', false);// autocomplete won't be triggered if input contains any characters present in this string$this->set('disableChars', '');// when true, each word will be isolated to a separate searchFields=searchValue, which// enables it to duplicate ~= operator behavior while using a %= operator.$this->set('useAndWords', false);// Force Language when applicable$this->set('lang_id', 0);// Whether to allow unpublished pages (null=not set, 0|false=no, 1|true=yes)$this->set('allowUnpub', null);}/*** Render a selected list item** @param string $label* @param string $value* @param string $class* @return string**/protected function ___renderListItem($label, $value, $class = '') {if($class) $class = " $class";if(strpos($label, '&') !== false) $label = $this->wire('sanitizer')->unentities($label);$label = $this->wire('sanitizer')->entities($label);$out ="<li class='ui-state-default$class'>" ."<i class='itemSort fa fa-arrows fa-fw'></i> " ."<span class='itemValue'>$value</span>" ."<span class='itemLabel'>$label</span> " ."<a class='itemRemove' href='#'><i class='fa fa-trash'></i></a>" ."</li>";return $out;}/*** Render the selected items list** @return string**/protected function ___renderList() {$out = "<ol id='{$this->id}_items' data-id='{$this->id}' data-name='{$this->name}'>" .$this->renderListItem("Label", "1", "itemTemplate");foreach($this->value as $page_id) {if(!$page_id) continue;$page = $this->pages->get((int) $page_id);if(!$page || !$page->id) continue;$value = $this->labelFieldFormat ? $page->getText($this->labelFieldFormat, true, false) : $page->get($this->labelFieldName);$value = strip_tags($value);if(!strlen($value)) $value = $page->get('title|name');$out .= $this->renderListItem($value, $page->id);}$out .= "</ol>";return $out;}/*** Render the autocompletion widget**/public function ___render() {/** @var Sanitizer $sanitizer */$sanitizer = $this->wire('sanitizer');if($this->maxSelectedItems == 1) $this->useList = false;$out = $this->useList ? $this->renderList() : '';$value = implode(',', $this->value);$url = $this->getAjaxUrl();// convert our list of search fields to a CSV string for use in the ProcessPageSearch query$searchField = '';$searchFields = str_replace(array(',', '|'), ' ', $this->searchFields);foreach(explode(' ', $searchFields) as $key => $name) {$name = trim($name);// @esrch pr#994 --if(strpos($name, '.')) {list($name, $subname) = explode('.', $name);$name = $sanitizer->fieldName($name);$subname = $sanitizer->fieldName($subname);if($name && $subname) $name .= "-$subname";// -- pr#994} else {$name = $sanitizer->fieldName($name);}if($name) $searchField .= ($searchField ? ',' : '') . $name;}if(!$searchField) $searchField = 'title';$addNote = $this->_('Hit enter to add as new item');$labelField = $this->labelFieldFormat ? "autocomplete_$this->name" : $this->labelFieldName;$operator = $this->operator;$id = $this->id;$max = (int) $this->maxSelectedItems;$class = 'ui-autocomplete-input ' . ($this->useList ? 'has_list' : 'no_list');if($this->useAndWords) $class .= " and_words";if($this->allowAnyValue) $class .= " allow_any";$disableChars = $this->disableChars;if($disableChars) {$hasDoubleQuote = strpos($disableChars, '"') !== false;$hasSingleQuote = strpos($disableChars, "'") !== false;if($hasDoubleQuote && $hasSingleQuote) {$this->error("disableChars cannot have both double and single quotes");$disableChars = str_replace('"', '', $disableChars);}if($hasSingleQuote) $disableChars = "data-disablechars=\"$disableChars\" ";else $disableChars = "data-disablechars='$disableChars' ";}$attrs ="data-max='$max' " ."data-url='" . $sanitizer->entities($url) . "' " ."data-label='" . $sanitizer->entities($labelField) . "' " ."data-search='$searchField' " ."data-operator='$operator'";$textValue = '';$remove = '';if(!$this->useList) {if(count($this->value)) {$item = $this->wire('pages')->getById($this->value)->first();if($item && $item->id) {$textValue = $this->labelFieldFormat ? $item->getText($this->labelFieldFormat, true, false) : $item->get($labelField);if(!strlen($textValue)) $textValue = $item->get('title|name');$textValue = strip_tags($textValue);if(strpos($textValue, '&') !== false) $textValue = $sanitizer->unentities($textValue);$textValue = $sanitizer->entities($textValue);}}$remove = "<i class='fa fa-fw fa-times-circle InputfieldPageAutocompleteRemove'></i>";}$addingLabel = $sanitizer->entities1($this->_('New item:'));$dataClass = $sanitizer->entities(trim('InputfieldPageAutocompleteData ' . $this->attr('class')));$out .= <<< _OUT<p><input type='hidden' name='{$this->name}[]' id='$id' class='$dataClass' value='$value' $attrs/><input type='text' data-parent-input='$id' id='{$id}_input' class='$class' value='$textValue' $disableChars/><i class='fa fa-fw fa-angle-double-right InputfieldPageAutocompleteStatus'></i>$remove<span class='notes InputfieldPageAutocompleteNote' data-adding='$addingLabel'><br />$addNote</span></p>_OUT;/*<script>$(document).ready(function() {InputfieldPageAutocomplete.init('$id', '$url', '$labelField', '$searchField', '$operator');console.log('initAutocomplete: $id');});</script>*/return $out;}/*** Convert the CSV string provided in the $input to an array of ints needed for this fieldtype** @param WireInputData $input* @return $this**/public function ___processInput(WireInputData $input) {parent::___processInput($input);$value = $this->attr('value');if(is_array($value)) $value = reset($value);$value = trim($value);if(strpos($value, ",") !== false) $value = explode(",", $value);else if($value) $value = array($value);else $value = array();foreach($value as $k => $v) {if(empty($v)) {unset($value[$k]);} else {$value[$k] = (int) $v;}}$this->attr('value', $value);return $this;}/*** Get the AJAX search URL that will be queried (minus the actual term)** This URL is focused on using the AJAX API from ProcessPageSearch**/protected function getAjaxUrl() {$pipe = '%7C'; // encoded pipe "|"$selector = $this->findPagesSelector;if($this->parent_id) {if($selector) {// if a selector was specified, AND a parent, then we'll use the parent as a root$selector .= ",has_parent={$this->parent_id}";} else {// otherwise matches must be direct children of the parent$selector = "parent_id={$this->parent_id}";}}if(count($this->template_ids)) {$selector .= ",templates_id=" . implode($pipe, $this->template_ids);} else if($this->template_id) {$selector .= ",templates_id={$this->template_id}";}if($this->lang_id) {$selector .= ",lang_id=" . (int) $this->lang_id;}$allowUnpub = $this->getSetting('allowUnpub');if(!is_null($allowUnpub) && !$allowUnpub) {$selector .= ",status<" . Page::statusUnpublished;}// allow for full site matchesif(!strlen($selector)) $selector = "id>0";// match no more than 50, unless selector specifies it's own limitif(strpos($selector, 'limit=') === false) $selector .= ",limit=50";// replace non-escaped commas with ampersands$selector = preg_replace('/(?<!\\\\),\s*/', '&', $selector);if(strpos($selector, '.')) {// replace things like children.count with children-count since "." is not allowed in URL var names$selector = preg_replace('/(^|&)([_a-zA-Z0-9]+)\.([_a-zA-Z0-9]+)=/', '$1$2-$3=', $selector);}// specify what label field we want to retrieveif($this->labelFieldFormat) {$name = "autocomplete_" . $this->attr('name');$this->wire('modules')->get('ProcessPageSearch')->setDisplayFormat($name, $this->labelFieldFormat, true);$selector .= "&format_name=$name";}$selector .= "&get=" . $this->labelFieldName;// replace any pipes with encoded versionif(strpos($selector, '|') !== false) $selector = str_replace('|', $pipe, $selector);return $this->config->urls->admin . "page/search/for?" . $selector;}/*** Install the autocomplete module** Make sure we're in InputfieldPage's list of valid page selection widgets**/public function ___install() {$data = $this->wire('modules')->getModuleConfigData('InputfieldPage');$data['inputfieldClasses'][] = $this->className();$this->wire('modules')->saveModuleConfigData('InputfieldPage', $data);}/*** Uninstall the autocomplete module** Remove from InputfieldPage's list of page selection widgets**/public function ___uninstall() {$data = $this->wire('modules')->getModuleConfigData('InputfieldPage');foreach($data['inputfieldClasses'] as $key => $value) {if($value == $this->className()) unset($data['inputfieldClasses'][$key]);}$this->wire('modules')->saveModuleConfigData('InputfieldPage', $data);}/*** Provide configuration options for modifying the behavior when paired with InputfieldPage**/public function ___getConfigInputfields() {$inputfields = parent::___getConfigInputfields();/** @var InputfieldRadios $field */$field = $this->modules->get('InputfieldRadios');$field->setAttribute('name', 'operator');$field->label = $this->_('Autocomplete search operator');$field->description = $this->_("The search operator that is used in the API when performing autocomplete matches.");$field->notes = $this->_("If you aren't sure what you want here, leave it set at the default: *=");$field->required = false;$operators = Selectors::getOperators(array('compareType' => Selector::compareTypeFind,'getIndexType' => 'operator','getValueType' => 'verbose',));foreach($operators as $operator => $info) {if($operator === '#=') continue;$opLabel = str_replace('*', '\*', $operator);$field->addOption($operator, "`$opLabel` **$info[label]** — $info[description]");}$field->addOption('=', "`=` **" . SelectorEqual::getLabel() . "** — " . SelectorEqual::getDescription());$field->attr('value', $this->operator);$field->collapsed = Inputfield::collapsedNo;$inputfields->add($field);/** @var InputfieldText $field */$field = $this->modules->get('InputfieldText');$field->attr('name', 'searchFields');$field->label = $this->_('Fields to query for autocomplete');$field->description = $this->_('Enter the names of the fields that should have their text queried for autocomplete matches.');$field->description .= ' ' . $this->_('Typically this would just be the title field, but you may add others by separating each with a space.');$field->description .= ' ' . $this->_('Note that this is different from the “label field” because you are specifying what fields will be searched, not what fields will be shown.');$field->collapsed = Inputfield::collapsedNo;$field->attr('value', $this->searchFields);$notes = $this->_('Indexed text fields include:');foreach($this->wire('fields') as $f) {if(!$f->type instanceof FieldtypeText) continue;$notes .= ' ' . $f->name . ',';}$field->notes = rtrim($notes, ',');$inputfields->add($field);return $inputfields;}}