-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathTypeahead.php
206 lines (192 loc) · 7.92 KB
/
Typeahead.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<?php
/**
* @copyright Copyright © Kartik Visweswaran, Krajee.com, 2014
* @package yii2-widgets
* @subpackage yii2-widget-typeahead
* @version 1.0.0
*/
namespace kartik\typeahead;
use Yii;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\helpers\ArrayHelper;
use yii\base\InvalidConfigException;
use yii\web\JsExpression;
/**
* Typeahead widget is a Yii2 wrapper for the Twitter typeahead.js plugin. This
* input widget is a jQuery based replacement for text inputs providing search
* and typeahead functionality. It is inspired by twitter.com's autocomplete search
* functionality and based on Twitter's typeahead.js which Twitter mentions as
* a fast and fully-featured autocomplete library.
*
* This is an advanced implementation of the typeahead.js plugin included with the
* Bloodhound suggestion engine.
*
* @author Kartik Visweswaran <[email protected]>
* @since 1.0
* @see http://twitter.github.com/typeahead.js/examples
*/
class Typeahead extends TypeaheadBasic
{
/**
* @var array dataset an object that defines a set of data that hydrates suggestions.
* It consists of the following special variable settings:
* - local: array configuration for the [[local]] list of datums. You must set one of
* [[local]], [[prefetch]], or [[remote]].
* - displayKey: string the key used to access the value of the datum in the datum
* object. Defaults to 'value'.
* - prefetch: array configuration for the [[prefetch]] options object.
* - remote: array configuration for the [[remote]] options object.
* - limit: integer the max number of suggestions from the dataset to display for
* a given query. Defaults to 8.
* - dupDetector: JsExpression, a function with the signature (remoteMatch, localMatch) that returns
* true if the datums are duplicates or false otherwise. If not set, duplicate detection will not
* be performed.
* - sorter – JsExpression, a compare function used to sort matched datums for a given query.
* - templates: array the templates used to render suggestions.
*/
public $dataset = [];
/**
* @var bool whether to register and use Handle Bars Template compiler plugin.
* Defaults to `true`.
*/
public $useHandleBars = true;
/**
* @var array the HTML attributes for the input tag.
*/
public $options = [];
/**
* @var string the generated Bloodhound script
*/
protected $_bloodhound;
/**
* @var string the generated Json encoded Dataset script
*/
protected $_dataset;
/**
* Runs the widget
*
* @return string|void
* @throws \yii\base\InvalidConfigException
*/
public function run()
{
if (empty($this->dataset) || !is_array($this->dataset)) {
throw new InvalidConfigException("You must define the 'dataset' property for Typeahead which must be an array.");
}
if (!is_array(current($this->dataset))) {
throw new InvalidConfigException("The 'dataset' array must contain an array of datums. Invalid data found.");
}
$this->validateConfig();
$this->initDataset();
$this->registerAssets();
$this->initOptions();
echo Html::tag('div', $this->getInput('textInput'), $this->container);
}
/**
* @return void Validate if configuration is valid
* @throws \yii\base\InvalidConfigException
*/
protected function validateConfig()
{
foreach ($this->dataset as $datum) {
if (empty($datum['local']) && empty($datum['prefetch']) && empty($datum['remote'])) {
throw new InvalidConfigException("No data source found for the Typeahead. The 'dataset' array must have one of 'local', 'prefetch', or 'remote' settings enabled.");
}
}
}
/**
* Initialize the data set
*/
protected function initDataset()
{
$index = 1;
$this->_bloodhound = '';
$this->_dataset = '';
$dataset = [];
foreach ($this->dataset as $datum) {
$dataVar = strtr(strtolower($this->options['id'] . '_data_' . $index), ['-' => '_']);
$this->_bloodhound .= "var {$dataVar} = new Bloodhound(" .
Json::encode($this->parseSource($datum)) . ");\n" .
"{$dataVar}.initialize();\n";
$d = ['name' => $dataVar, 'source' => new JsExpression($dataVar . '.ttAdapter()')];
if (!empty($datum['displayKey'])) {
$d = ArrayHelper::merge(['displayKey' => $datum['displayKey']], $d);
}
if (!empty($datum['templates'])) {
$d = ArrayHelper::merge(['templates' => $datum['templates']], $d);
}
$dataset[] = $d;
$index++;
}
$this->_dataset .= Json::encode($dataset);
}
/**
* Parses the data source array and prepares the bloodhound configuration
*
* @param array $source the source data
* @return array parsed formatted source
*/
protected function parseSource($source = [])
{
$key = ArrayHelper::getValue($source, 'displayKey', 'value');
$datumTokenizer = ArrayHelper::remove($source, 'datumTokenizer', new JsExpression("Bloodhound.tokenizers.obj.whitespace('{$key}')"));
if (!$datumTokenizer instanceof JsExpression) {
$datumTokenizer = new JsExpression($datumTokenizer);
}
$queryTokenizer = ArrayHelper::remove($source, 'queryTokenizer', new JsExpression('Bloodhound.tokenizers.whitespace'));
if (!$queryTokenizer instanceof JsExpression) {
$queryTokenizer = new JsExpression($queryTokenizer);
}
$limit = ArrayHelper::remove($source, 'limit', 8);
if (!is_numeric($limit)) {
$limit = 8;
}
if (!empty($source['dupDetector']) && !$source['dupDetector'] instanceof JsExpression) {
$dupDetector = new JsExpression($source['dupDetector']);
}
if (!empty($source['sorter']) && !$source['sorter'] instanceof JsExpression) {
$sorter = new JsExpression($source['sorter']);
}
if (!empty($source['local']) && is_array($source['local'])) {
$local = new JsExpression('$.map(' . Json::encode(array_values($source['local'])) . ", function(v){ return{{$key}:v}; })");
} elseif (!empty($source['local'])) {
$local = ($source['local'] instanceof JsExpression) ? $source['local'] : new JsExpression($source['local']);
}
if (!empty($source['prefetch'])) {
$prefetch = $source['prefetch'];
if (!is_array($prefetch)) {
$prefetch = ['url' => $prefetch];
}
}
if (!empty($source['remote'])) {
$remote = $source['remote'];
$hint = 'jQuery("#' . $this->options['id'] . '")';
/* Add a spinning indicator for remote calls */
$r = is_array($remote) ? $remote : ['url' => $remote];
if (empty($r['ajax']['beforeSend'])) {
$r['ajax']['beforeSend'] = new JsExpression("function(){{$hint}.addClass('loading');}");
}
if (empty($r['ajax']['complete'])) {
$r['ajax']['complete'] = new JsExpression("function(){{$hint}.removeClass('loading');}");
}
$remote = $r;
}
return compact('datumTokenizer', 'queryTokenizer', 'limit', 'dupDetector', 'sorter', 'local', 'prefetch', 'remote');
}
/**
* Registers the needed assets
*/
public function registerAssets()
{
$view = $this->getView();
TypeaheadAsset::register($view);
if ($this->useHandleBars) {
TypeaheadHBAsset::register($view);
}
$this->registerPluginOptions('typeahead');
$view->registerJs($this->_bloodhound);
$view->registerJs('jQuery("#' . $this->options['id'] . '").typeahead(' . $this->_hashVar . ',' . $this->_dataset . ');');
$this->registerPluginEvents($view);
}
}