Source of file TypeAheadField.php
Size: 15,369 Bytes - Last Modified: 2021-12-23T10:05:09+00:00
/var/www/docs.ssmods.com/process/src/src/TypeAheadField.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 | <?php use SS_HTTPRequest as HTTPRequest; class TypeAheadField extends TextField { private static $allowed_actions = [ 'suggestion', 'prefetch', ]; /** @var string Class we are searching (when using @SS_List) */ public $sourceClass; /** @var string|array Search filter when using a @SS_List */ public $sourceField = null; /** @var string Name of the field to use as a suggestion */ public $refField = 'Title'; /** @var string Name of the field to use as a value */ public $valField = 'Title'; /** * List used to search in database (if not provided, uses source class and source field instead) * Will also accept a URL to override field suggestion * @var SS_List|array|string|Closure */ public $sourceList; /** @var string|boolean The url to use as a live search */ public $suggestURL; /** * Maximum number of search results to display per search * * @var integer */ public $limit = 10; /** * Minimum number of characters that a search will act on * * @var integer */ public $minSearchLength = 2; /** * Flag indicating whether a selection must be made from the existing list (creating a tag field) * By default free text entry is not allowed. * * @var boolean */ public $requireSelection = false; /** * Prefetch a number of results so user has some to select from * Will also accept a URL to override field prefetch * * @var integer|String */ public $prefetch = 10; /** @var array Options that will be disabled but still display */ public $disabledOptions = []; /** @var array Options that will always be selected */ public $lockedOptions = []; /** @var string|boolean Sort array by key/value/false */ public $sortArray = false; /** @var string Query key */ public $queryKey = 'q'; /** @var string Separator for keys */ public $separator = '|'; /** @var Callable Callback for parsing results */ protected $resultsCallback; protected $disallowedSearchTypes = [ 'Int', 'Date', 'Boolean', 'Decimal', 'Double', 'Float', 'Int', 'Percentage', 'Time', 'Year', ]; function __construct( $name, $title = null, $value = null, $sourceList = null, $sourceField = null, $refField = '', $valField = '' ) { // set source $this->sourceList = $sourceList; $this->sourceField = $sourceField; if ($refField) { $this->refField = $refField; } if ($valField) { $this->valField = $valField; } // construct the TextField parent::__construct($name, $title, $value); $this->value = $value; } /** * Set source class * * @param $value * * @return $this */ function setSourceClass($value) { $this->sourceClass = $value; return $this; } function setSourceField($value) { $this->sourceField = $value; return $this; } function setSourceList($value) { $this->sourceList = $value; return $this; } public function setSource($items) { if (is_array($items) || ($items instanceof ArrayAccess)) { $this->sourceList = $items; } else { $this->sourceClass = $items; } return $this; } function getSourceList() { if ($this->sourceList && is_string($this->sourceList)) { return null; } if (!$this->sourceList) { if ($class = $this->SourceClass) { $this->sourceList = DataList::create($class); } } return $this->sourceList; } public function getSourceClass() { if ($class = $this->sourceClass) { return $class; } $form = $this->getForm(); if (!$form) { return null; } $record = $form->getRecord(); if (!$record) { return null; } return $record->ClassName; } function getSuggestURL() { if ($this->sourceList && is_string($this->sourceList)) { return $this->sourceList; } if ($this->suggestURL !== null) { return $this->suggestURL; } elseif ($this->form) { return $this->Link('suggestion'); } else { return ''; } } function setSuggestURL($val = null) { $this->suggestURL = $val; return $this; } function getPrefetchURL() { if ($this->prefetch && is_string($this->prefetch)) { return $this->prefetch; } return $this->prefetch && $this->form ? $this->Link('prefetch') : null; } function setPrefetch($val = null) { $this->prefetch = $val; return $this; } function setMinimumSearchLength($val = 2) { $this->minSearchLength = $val; return $this; } function requireSelection($flag = true) { $this->requireSelection = $flag; return $this; } function setQueryKey($key = 'q') { $this->queryKey = $key; return $this; } function setResultsCallback($callback = null) { $this->resultsCallback = $callback; return $this; } function getAttributes() { $this->extraClasses[] = 'text'; if (isset($_GET)) { $query = $_GET; if (isset($query['url'])) { unset($query['url']); } $query = http_build_query($query); } else { $query = ''; } $attributes = array_merge( [ 'data-suggest-url' => $this->SuggestURL ? Controller::join_links($this->SuggestURL, '?' . $query, sprintf('?%s=%%QUERY', $this->queryKey)) : false, 'data-prefetch-url' => $this->PrefetchURL ? Controller::join_links($this->PrefetchURL, '?' . $query) : false, 'data-min-length' => $this->minSearchLength, 'data-require-selection' => $this->requireSelection, 'data-name' => strtolower($this->ID()), 'data-templates.empty' => _t('TypeAheadField.NO_MATCHES', 'No matches found'), ], parent::getAttributes(), [ 'autocomplete' => 'off', ] ); if (!$this->form || (!$this->SuggestURL && !$this->PrefetchURL) || is_array($this->SourceList)) { if ($list = $this->SourceList) { $results = $this->results('', $list, null, false); } else { $results = []; } $attributes['data-local'] = json_encode($results); } if (is_array($this->SourceList)) { unset($attributes['data-suggest-url']); unset($attributes['data-prefetch-url']); } return $attributes; } function Field($properties = []) { if (!$this->config()->exclude_js) { $this->includeJs(); } if (!$this->config()->exclude_css) { $this->includeCss(); } return parent::Field($properties); } function suggestion(HTTPRequest $r) { $results = []; $list = $this->SourceList; if (!$list) { $response = new SS_HTTPResponse(json_encode($results), 200, 'fail'); $response->addHeader('Content-type', 'application/json'); return $response; } if ($this->limit === false) { $limit = null; } else { $limit = $this->limit ? $this->limit : 10; } // input if ($this->resultsCallback) { $callbacks = [ 'resultsToMap' => function ($list, $valField = 'ID', $refField = 'Title') { return $this->resultsToMap($list, $valField, $refField); }, 'resultToMap' => function ($id, $text, $keyField = 'id', $valField = 'text') { return $this->resultToMap($id, $text, $keyField, $valField); }, 'getValueFromItem' => function ($item, $setting = '', $implodeWith = '|', $clearEmpty = false) { return $this->getValueFromItem($item, $setting, $implodeWith, $clearEmpty); }, ]; $resultsCallback = $this->resultsCallback; $results = $resultsCallback(Convert::raw2sql($r->getVar($this->queryKey)), $list, null, $limit, $callbacks); } else { $results = $this->results(Convert::raw2sql($r->getVar($this->queryKey)), $list, null, $limit); } $response = new SS_HTTPResponse(json_encode($results), 200, ''); $response->addHeader('Content-type', 'application/json'); return $response; } function prefetch(HTTPRequest $r = null) { if ($this->prefetch === true) { $limit = null; } else { $limit = $this->prefetch ? $this->prefetch : 10; } if ($list = $this->SourceList) { $results = $this->results('', $list, null, $limit); } else { $results = []; } $response = new SS_HTTPResponse(json_encode($results), 200, ''); $response->addHeader('Content-type', 'application/json'); return $response; } public function results($q = '', $list = null, $class = null, $limit = 10) { $list = $this->getListToUse($list, $q, $limit); $class = $class ? $class : ($list && !is_array($list)) ? $list->dataClass() : $this->SourceClass; if (is_array($list)) { $results = $this->filterArray($q, $list, $class, $limit); } else { $results = $this->filterList($q, $list, $class, $limit); } return $results; } public function filterArray($q, $list, $class = null, $limit = null) { $results = []; $noOfResults = 0; if(is_array($q) && !empty($q['term'])) { $q = $q['term']; } if ($class && $search = $this->scaffoldSearchFields($class)) { $context = explode(':', reset($search)); $pattern = ''; if ($q && isset($context[1])) { switch ($context[1]) { case 'StartsWith': $pattern = '/^' . $q . '/'; break; case 'EndsWith': $pattern = '/' . $q . '$/'; break; default: $pattern = '/' . $q . '/'; break; } } } else { $pattern = $q ? '/^' . $q . '/' : ''; } foreach ($list as $key => $item) { if ($limit && $noOfResults >= $limit) { break; } if (ArrayLib::is_associative($list) && is_array($item)) { $result = $this->filterArray($q, $item, $class, $limit); if ($noOfResult = count($result)) { if ($limit && ($noOfResults + $noOfResult) > $limit) { array_splice($result, 0, ($noOfResult - $noOfResults)); } $noOfResults += $noOfResult; $results[] = $this->resultGroupToMap($key, $result); } } elseif (is_array($item)) { if (!($value = $this->getValueFromItem($item, $this->refField, ' - ', true))) { $value = ''; } if (!($key = $this->getValueFromItem($item, $this->valField))) { $key = $noOfResults; } if (!is_string($value)) { continue; } if ($pattern && preg_match(preg_quote($pattern), $value)) { $results[] = $this->resultToMap($key, $value); $noOfResults++; } else { $results[] = $this->resultToMap($key, $value); $noOfResults++; } } else { $value = is_string($item) ? $item : $key; if (!is_string($value)) { continue; } if ($pattern && preg_match(preg_quote($pattern), $value)) { $results[] = $this->resultToMap($key, $value); $noOfResults++; } else { $results[] = $this->resultToMap($key, $value); $noOfResults++; } } } if ($this->sortArray) { $key = ($this->sortArray === 'key') ? 'id' : 'text'; usort($results, function ($result1, $result2) use ($key) { return strcmp($result1[$key], $result2[$key]); }); } return $results; } public function filterList($q, $list, $class, $limit = 10) { $results = []; $search = $this->scaffoldSearchFields($class); $sort = $this->refField ? $this->refField : strtok($search[0], ':'); $params = []; if ($q) { foreach ($search as $field) { $name = (strpos($field, ':') !== false) ? $field : "$field:StartsWith"; $params[$name] = $q; } $resulting = $list ->filterAny($params) ->sort($sort, 'ASC') ->limit($limit); } else { $resulting = $list ->sort($sort, 'ASC') ->limit($limit); } if ($resulting->exists()) { $results = $this->resultsToMap($resulting); } return $results; } public function resultsToMap($list, $valField = 'ID', $refField = 'Title') { $valField = $this->valField ? $this->valField : $valField; $refField = $this->refField ? $this->refField : $refField; $results = []; foreach ($list as $result) { if ($result->hasMethod('canView') && !$result->canView()) { continue; } $results[] = $this->resultToMap($this->getValueFromItem($result, $valField), $this->getValueFromItem($result, $refField, ' - ', true)); } return $results; } public function resultToMap($id, $text, $keyField = 'id', $valField = 'text') { return [ $keyField => $id, $valField => (string)$text, 'disabled' => in_array($id, $this->disabledOptions), 'locked' => in_array($id, $this->lockedOptions), ]; } public function resultGroupToMap($title, $children, $valField = 'text') { return [ $valField => $title, 'children' => $children, ]; } public function getValueFromItem($item, $setting = '', $implodeWith = '|', $clearEmpty = false) { if (!$setting) { $setting = $this->valField; } $valField = is_string($setting) && strpos($setting, $this->separator) !== false ? explode($this->separator, $setting) : $setting; if (is_array($valField)) { $value = []; if ($item instanceof ViewableData) { foreach ($valField as $field) { $value[] = $item->$field; } } elseif ($item instanceof stdClass) { foreach ($valField as $field) { $value[] = isset($item->$field) ? $item->$field : ''; } } elseif (is_array($item)) { foreach ($valField as $field) { $value[] = isset($item[$field]) ? $item[$field] : ''; } } if ($clearEmpty) { $value = array_filter($value); } return implode($implodeWith, $value); } if (is_array($item) && isset($item[$valField])) { return $item[$valField]; } elseif (is_object($item) && isset($item->$valField)) { return $item->$valField; } else { return $item; } } public function validate($validator) { if ($this->requireSelection && !($this->SourceList instanceof Closure)) { $results = $this->results($this->value); if (!$results || !count($results)) { $validator->validationError( $this->name, _t('TypeAheadField.INVALID', 'Invalid value'), 'validation', false ); return false; } } return parent::validate($validator); } protected function getListToUse($list = null, $q = '', $limit = 0) { $list = $list ? $list : $this->SourceList; if ($list instanceof Closure) { $list = $list($q, $limit); } return $list; } protected function scaffoldSearchFields($dataClass) { if ($this->sourceField) { return $this->sourceField; } $obj = singleton($dataClass); $fields = null; if ($fieldSpecs = $obj->searchableFields()) { foreach ($fieldSpecs as $name => $spec) { $casting = explode('(', $obj->castingHelper($name)); if (isset($casting[0]) && in_array($casting[0], $this->disallowedSearchTypes)) { continue; } if (is_array($spec) && array_key_exists('filter', $spec)) { $filter = preg_replace('/Filter$/', '', $spec['filter']); $fields[] = "{$name}:{$filter}"; } else { $fields[] = $name; } } } if (is_null($fields)) { if ($this->valField && $obj->hasDatabaseField($this->valField)) { $fields = [$this->valField . ':StartsWith']; } elseif ($obj->hasDatabaseField('Title')) { $fields = ['Title:StartsWith']; } elseif ($obj->hasDatabaseField('Name')) { $fields = ['Name:StartsWith']; } } return $fields; } protected function includeJs() { Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(SS_MWM_FORMFIELDS_DIR . '/thirdparty/js/typeahead.bundle.js'); Requirements::javascript(SS_MWM_FORMFIELDS_DIR . '/js/typeahead.init.js'); } protected function includeCss() { Requirements::css(SS_MWM_FORMFIELDS_DIR . '/css/typeahead.field.css'); } } |