Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typeahead and Bloodhound support #5

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
language: php

php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm

install:
- composer self-update
- composer global require "fxp/composer-asset-plugin:1.0.0"
- composer global require "fxp/composer-asset-plugin:1.1.4"
- composer install

before_script:
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,51 @@ use wbraganca\tagsinput\TagsinputWidget;
]
]) ?>

<?php echo $form->field($model, 'places')->widget(TagsinputWidget::classname(), [
'clientOptions' => [
"itemValue" => 'name',
"itemText" => 'name',
],
'dataset' => [
[
'remote' => [
'url' => Url::to(['get-countries']). '?q=%QUERY',
'wildcard' => '%QUERY'
],
'datumTokenizer' => "Bloodhound.tokenizers.obj.whitespace('name')",
'displayKey' => 'name',
'limit' => 10,
'templates' => [
'header' => '<h3 class="name">Country</h3>'
]
],
[
'remote' => [
'url' => Url::to(['get-cities']). '?q=%QUERY',
'wildcard' => '%QUERY'
],
'datumTokenizer' => "Bloodhound.tokenizers.obj.whitespace('name')",
'displayKey' => 'name',
'limit' => 10,
'templates' => [
'header' => '<h3 class="name">City</h3>'
]
],
[
'remote' => [
'url' => Url::to(['get-states']). '?q=%QUERY',
'wildcard' => '%QUERY'
],
'datumTokenizer' => "Bloodhound.tokenizers.obj.whitespace('name')",
'displayKey' => 'name',
'limit' => 10,
'templates' => [
'header' => '<h3 class="name">State</h3>'
]
]
]
]) ?>

```

For more options, visit: http://bootstrap-tagsinput.github.io/bootstrap-tagsinput/examples/
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"require": {
"yiisoft/yii2": "~2.0",
"yiisoft/yii2-bootstrap": "~2.0.0",
"bower-asset/typeahead.js": "0.10.* | ~0.11.0",
"bower-asset/bootstrap-tagsinput": "~0.6"
},
"require-dev": {
Expand Down
3 changes: 2 additions & 1 deletion src/TagsinputAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class TagsinputAsset extends \yii\web\AssetBundle

public $depends = [
'yii\web\JqueryAsset',
'yii\bootstrap\BootstrapAsset'
'yii\bootstrap\BootstrapPluginAsset',
'wbraganca\tagsinput\TypeaheadAsset'
];
}
190 changes: 188 additions & 2 deletions src/TagsinputWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use yii\helpers\Html;
use yii\helpers\Json;
use yii\web\View;
use yii\web\JsExpression;
use yii\helpers\ArrayHelper;
use yii\base\InvalidConfigException;

/**
* The yii2-tagsinput is a Yii 2 wrapper for bootstrap-tagsinput.
Expand All @@ -27,6 +30,12 @@ class TagsinputWidget extends \yii\widgets\InputWidget
* @see http://timschlechter.github.io/bootstrap-tagsinput/examples/#options
*/
public $clientOptions = [];

/**
* @var array the JQuery plugin options for the typeahead.
* @see http://twitter.github.com/typeahead.js/examples
*/
public $typeaheadOptions = [];
/**
* @var array the HTML attributes for the input tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
Expand All @@ -37,20 +46,110 @@ class TagsinputWidget extends \yii\widgets\InputWidget
*/
protected $_hashVar;

/**
* @var array dataset an object that defines a set of data that hydrates suggestions.
* For TypeaheadBasic, this is a single dimensional array consisting of following settings.
* For Typeahead, this is a multi-dimensional array, with each array item being an array that
* consists of the following settings.
* - source: The backing data source for suggestions. Expected to be a function with the
* signature `(query, syncResults, asyncResults)`. This can also be a Bloodhound instance.
* If not set, this will be automatically generated based on the bloodhound specific
* properties in the next section below.
* - display: string the key used to access the value of the datum in the datum
* object. Defaults to 'value'.
* - async: boolean, lets the dataset know if async suggestions should be expected. Defaults to `true`.
* - limit: integer the max number of suggestions from the dataset to display for
* a given query. Defaults to 5.
* - templates: array the templates used to render suggestions.
* The following properties are bloodhound specific data configuration properties and not applicable
* for TypeaheadBasic. Its only applied for Typeahead.
* - local: array configuration for the [[local]] list of datums. You must set one of
* [[local]], [[prefetch]], or [[remote]].
* - prefetch: array configuration for the [[prefetch]] options object.
* - remote: array configuration for the [[remote]] options object.
* - initialize: true,
* - identify: defaults to _.stringify,
* - datumTokenizer: defaults to null,
* - queryTokenizer: defaults null,
* - sufficient: 5,
* - sorter: null,
*/
public $dataset = [];

/**
* @var string the generated Bloodhound script
*/
protected $_bloodhound;


/**
* @var string the generated HashPluginOptions script
*/
protected $_hashPluginOptions;

/**
* @var string the generated Json encoded Dataset script
*/
protected $_dataset;

/**
* @var bool whether default suggestions are enabled
*/
protected $_defaultSuggest = false;

/**
* @var array the bloodhound settings variables
*/
protected static $_bhSettings = [
'datumTokenizer',
'queryTokenizer',
'initalize',
'sufficient',
'sorter',
'identify',
'local',
'prefetch',
'remote'
];

/**
* @inheritdoc
*/
public function run()
{
$this->registerClientScript();

if(isset($this->dataset)) {
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->registerClientScript();
if ($this->hasModel()) {
echo Html::activeTextInput($this->model, $this->attribute, $this->options);
} else {
echo Html::textInput($this->name, $this->value, $this->options);
}
}

/**
* @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.");
}
}
}

/**
* Generates a hashed variable to store the plugin `clientOptions`. Helps in reusing the variable for similar
* options passed for other widgets on the same page. The following special data attribute will also be
Expand All @@ -62,10 +161,95 @@ public function run()
*/
protected function hashPluginOptions($view)
{
if(isset($this->typeaheadOptions)){
$this->clientOptions['typeaheadjs'][] = $this->typeaheadOptions;
}

if(isset($this->_dataset)){
$this->clientOptions['typeaheadjs'][] = $this->_dataset;
}

$encOptions = empty($this->clientOptions) ? '{}' : Json::encode($this->clientOptions);
$this->_hashVar = self::PLUGIN_NAME . '_' . hash('crc32', $encOptions);
$this->options['data-plugin-' . self::PLUGIN_NAME] = $this->_hashVar;
$view->registerJs("var {$this->_hashVar} = {$encOptions};\n", View::POS_HEAD);
$this->_hashPluginOptions = "var {$this->_hashVar} = {$encOptions};";
// $view->registerJs("var {$this->_hashVar} = {$encOptions};\n", View::POS_END);
}

/**
* 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 .= $this->parseSource($dataVar, $datum) . "\n";
$d = $datum;
$d['name'] = $dataVar;
if (empty($d['source'])) {
if ($this->_defaultSuggest) {
$sug = Json::encode($this->defaultSuggestions);
$sugVar = 'kvTypData_' . hash('crc32', $sug);
$this->getView()->registerJs("var {$sugVar} = {$sug};", View::POS_HEAD);
$source = "function(q,s){if(q===''){s({$dataVar}.get({$sugVar}));}else{{$dataVar}.search(q,s);}}";
} else {
$source = "{$dataVar}.ttAdapter()";
}
$d['source'] = new JsExpression($source);
}
$dataset[] = $d;
$index++;
}
$this->_dataset = $dataset;
}

/**
* Parses a variable and force converts it to JsExpression
* @param mixed $expr
* @return JsExpression
*/
protected static function parseJsExpr($expr)
{
return ($expr instanceof JsExpression) ? $expr : new JsExpression($expr);
}

/**
* Parses the data source array and prepares the bloodhound configuration
*
* @param string $dataVar the variable to store the Bloodhound instance
* @param array $source the source data
* @return string the prepared bloodhound configuration
*/
protected function parseSource($dataVar, &$source)
{
$out = [];
$defaultToken = new JsExpression("Bloodhound.tokenizers.whitespace");
foreach (self::$_bhSettings as $key) {
if ($key === 'datumTokenizer' || $key === 'queryTokenizer') {
$out[$key] = self::parseJsExpr(ArrayHelper::remove($source, $key, $defaultToken));
}
if (isset($source[$key])) {
$out[$key] = $source[$key];
if ($key === 'local') {
$local = Json::encode($source[$key]);
$localVar = 'kvTypData_' . hash('crc32', $local);
$this->getView()->registerJs("var {$localVar} = {$local};", View::POS_HEAD);
$out[$key] = new JsExpression($localVar);
} elseif ($key === 'prefetch') {
$prefetch = $source[$key];
if (!is_array($prefetch)) {
$prefetch = ['url' => $prefetch];
}
$out[$key] = $prefetch;
}
unset($source[$key]);
}
}
return "var {$dataVar} = new Bloodhound(" . Json::encode($out) . ");";
}

/**
Expand All @@ -77,6 +261,8 @@ public function registerClientScript()
$view = $this->getView();
$this->hashPluginOptions($view);
$id = $this->options['id'];
$view->registerJs("{$this->_bloodhound}\n", View::POS_END);
$view->registerJs("{$this->_hashPluginOptions}\n", View::POS_END);
$js .= '$("#' . $id . '").' . self::PLUGIN_NAME . "(" . $this->_hashVar . ");\n";
TagsinputAsset::register($view);
$view->registerJs($js);
Expand Down
25 changes: 25 additions & 0 deletions src/TypeaheadAsset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/**
* @link https://github.com/wbraganca/yii2-tagsinput
* @copyright Copyright (c) 2015 Wanderson Bragança
* @license https://github.com/wbraganca/yii2-tagsinput/blob/master/LICENSE
*/

namespace wbraganca\tagsinput;

/**
*
* @author Avikaresha Saha <[email protected]>
* @since 2.0
*/
class TypeAheadAsset extends yii\web\AssetBundle;
{
public $sourcePath = '@bower/typeahead.js/dist';
public $js = [
'typeahead.bundle.js',
];
public $depends = [
'yii\bootstrap\BootstrapAsset',
'yii\bootstrap\BootstrapPluginAsset',
];
}