1 <?php
2 /**
3 * WhDateRangePicker widget class
4 * Implementation of jQRangeSlider. A powerful slider for selecting value ranges, supporting dates and more.
5 * @see http://ghusse.github.io/jQRangeSlider
6 *
7 * @author Antonio Ramirez <amigo.cobos@gmail.com>
8 * @copyright Copyright © 2amigos.us 2013-
9 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
10 * @package YiiWheels.widgets.rangeslider
11 * @uses YiiStrap.helpers.TbArray
12 */
13 Yii::import('bootstrap.helpers.TbArray');
14
15 class WhRangeSlider extends CInputWidget
16 {
17
18 /**
19 * @var string lets you specify what type of jQRangeSlider you wish to display. Defaults to "range". Possible values
20 * are:
21 * - 'range'
22 * - 'editRange'
23 * - 'dateRange'
24 */
25 public $type = 'range';
26
27 /**
28 * @var bool lets you remove scrolling arrows on both sides of the slider. Defaults to true.
29 */
30 public $arrows = true;
31
32 /**
33 * @var mixed lets you specify the min bound value of the range. This value Defaults to 0.
34 * <strong>Note</strong> when using 'dateRange' type, this value can be string representing a javascript date. For
35 * example: `'minValue'=>'js:new Date( 2012, 0, 1)`'
36 */
37 public $minValue = 0;
38
39 /**
40 * @var mixed lets you specify the max bound value of the range. This value Defaults to 100.
41 * <strong>Note</strong> when using 'dateRange' type, this value can be a string representing a javascript date. For
42 * example: `'maxValue'=>'js:new Date( 2012, 0, 1)`'
43 */
44 public $maxValue = 100;
45
46 /**
47 * @var mixed lets you specify min value by default on construction of the widget. Defaults to 0.
48 * <strong>Note</strong> when using 'dateRange' type, this value can be a string representing a javascript date. For
49 * example: `'minDefaultValue'=>'js:new Date( 2012, 0, 1)`'
50 */
51 public $minDefaultValue = 0;
52
53 /**
54 * @var mixed lets you specify max valuee by default on construction of the widget. Defaults to 100.
55 * <strong>Note</strong> when using 'dateRange' type, this value can be a string representing a javascript date. For
56 * example: `'minDefaultValue'=>'js:new Date( 2012, 0, 1)`'
57 */
58 public $maxDefaultValue = 100;
59
60 /**
61 * @var int lets you specify the duration labels are shown after the user changed its values. Defaults to null.
62 * <strong>Note</strong>: This option can only be used in conjunction with `'valueLabels'=>'change'`
63 */
64 public $delayOut;
65
66 /**
67 * @var int lets you specify the animation length when displaying value labels. Similarly,`durationOut` allows to
68 * customize the animation duration when hiding those labels. Defaults to null.
69 * <strong>Note</strong>: This option can only be used in conjunction with `'valueLabels'=>'change'`
70 */
71 public $durationIn;
72
73 /**
74 * @var int lets you specify the animation length when hiding value labels. Similarly,`durationIn` allows to
75 * customize the animation duration when displaying those labels. Defaults to null.
76 * <strong>Note</strong>: This option can only be used in conjunction with `'valueLabels'=>'change'`
77 */
78 public $durationOut;
79
80 /**
81 * @var string a javascript function that lets you customize displayed label text.
82 * Example:
83 * ```
84 * 'formater'=>'js:function(val){
85 * var value = Math.round(val * 5) / 5,
86 * decimal = value - Math.round(val);
87 * return decimal == 0 ? value.toString() + ".0" : value.toString();
88 * }'
89 */
90 public $formatter;
91
92 /**
93 * @var int lets you specify minimum range length. It works in conjunction with `maxRange`.
94 * For instance, let's consider you want the user to choose a range of dates during 2012. You can constraint people
95 * to choose at least 1 week during this period. Similarly, you also can constraint the user to choose 90 days at
96 * maximum.
97 * When this option is activated, the slider constraints the user input. When minimum or maximum value is reached,
98 * the user cannot move an extremity further to shrink/enlarge the selected range.
99 */
100 public $minRange = 0;
101
102 /**
103 * @var int see `minRange` documentation above.
104 */
105 public $maxRange = 100;
106
107 /**
108 * @var int allows to customize values rounding, and graphically render this rounding. Considering you configured a
109 * slider with a step value of 10: it'll only allow your user to choose a value corresponding to minBound + 10n.
110 * <strong>Warning</strong> For the date slider there is a small variation, the following is an example with days:
111 *
112 * ```
113 * \\...
114 * 'step'=>array('days'=>2)
115 * \\...
116 */
117 public $step;
118
119 /**
120 * @var string allows to specify input types in edit slider. Possible values are 'text' (default) and 'number'.
121 * <strong>Note</strong>: This option is only available on `editRange` `type`.
122 */
123 public $inputType = 'text';
124
125 /**
126 * @var string lets you specify a display mode for value labels: `hidden`, `show`, or only shown when moving.
127 * Possible values are: show, hide and change.
128 */
129 public $valueLabels = 'show';
130
131 /**
132 * @var string allows to use the mouse wheel to `scroll` (translate) or `zoom` (enlarge/shrink) the selected area in
133 * jQRangeSlider. Defaults to null.
134 * <strong>Note</strong>: This option requires the plugin `jquery mousehwheel to be loaded`
135 */
136 public $wheelMode;
137
138 /**
139 * @var int lets you customize the speed of mouse wheel interactions. This parameter requires the `wheelMode` to be set.
140 */
141 public $wheelSpeed;
142
143 /**
144 * @var array The option scales lets you add a ruler with multiple scales to the slider background.
145 *
146 *
147 * ```
148 * 'scales' => array(
149 * // primary scale
150 * array(
151 * 'first'=>'js:function(val){return val;},
152 * 'next'=>'js:function(val){return val+10;}',
153 * 'stop'=>'js:function(val){return false;}',
154 * 'label'=>'js:function(val){return val;}',
155 * 'format'=> 'js:function(tickContainer, tickStart, tickEnd){
156 * tickContainer.addClass("myCustomClass");
157 * }'
158 * ),
159 * // secondary scale
160 * array(
161 * 'first'=>'js:function(val){return val;},
162 * 'next'=>'js:function(val){ if(val%10 === 9){return val+2;} return val + 1;}',
163 * 'stop'=>'js:function(val){return false;}',
164 * 'label'=>'js:function(val){return null;}',
165 * ),
166 * )
167 * ```
168 */
169 public $scales;
170
171 /**
172 * @var array string[] the events to monitor.
173 *
174 * Possible events:
175 * - valuesChanging
176 * - valuesChanged
177 * - userValuesChanged
178 *
179 * ### valuesChanging
180 * Use the event valuesChanging if you want to intercept events while the user is moving an element in the slider.
181 * Warning: Use this event wisely, because it is fired very frequently. It can have impact on performance. When
182 * possible, prefer the `valuesChanged` event.
183 *
184 * ```
185 * 'events' => array(
186 * "valuesChanging"=>'js:function(e, data){ console.log("Something moved. min: " + data.values.min +
187 * " max: " + data.values.max);})'
188 * )
189 * ```
190 *
191 * ### valuesChanged
192 * Use the event `valuesChanged` when you want to intercept events once values are chosen.
193 * This event is only triggered once after a move.
194 *
195 * ```
196 * 'events' => array(
197 * "valuesChanged", 'js:function(e, data){
198 * console.log("Values just changed. min: " + data.values.min + " max: " + data.values.max);
199 * }'
200 * )
201 * ````
202 *
203 * ### userValuesChanged
204 * Like the `valuesChanged` event, the `userValuesChanged` is fired after values finished changing. But, unlike the
205 * previous one, `userValuesChanged` is only fired after the user interacted with the slider. <strong>Not when you changed
206 * values programmatically</strong>.
207 *
208 * ```
209 * 'events' => array(
210 * "userValuesChanged", 'js:function(e, data){
211 * console.log("This changes when user interacts with slider");
212 * }'
213 * )
214 * ```
215 */
216 public $events;
217
218 /**
219 * @var string the theme to use with the slider. Supported values are "iThing" and "classic"
220 */
221 public $theme = 'iThing';
222
223 /**
224 * @var array
225 */
226 public $pluginOptions = array();
227 /**
228 * @var array the options to pass to the jQSlider
229 */
230 protected $options;
231
232 /**
233 * Widget's initialization
234 */
235 public function init()
236 {
237
238 $this->checkOptionAttribute($this->type, array('range', 'editRange', 'dateRange'), 'type');
239
240 $this->checkOptionAttribute($this->inputType, array('text', 'number'), 'inputType');
241
242 $this->checkOptionAttribute($this->valueLabels, array('show', 'hide', 'change'), 'valueLabels');
243
244 $this->checkOptionAttribute($this->theme, array('iThing', 'classic'), 'theme');
245
246 if ($this->wheelMode) {
247 $this->checkOptionAttribute($this->wheelMode, array('zoom', 'scroll'), 'wheelMode');
248 }
249 $this->attachBehavior('ywplugin', array('class' => 'yiiwheels.behaviors.WhPlugin'));
250 $this->buildOptions();
251 }
252
253 /**
254 * Widget's run method
255 */
256 public function run()
257 {
258 $this->renderField();
259 $this->registerClientScript();
260 }
261
262 /**
263 * Renders field and tag
264 */
265 public function renderField()
266 {
267 list($name, $id) = $this->resolveNameID();
268
269 if ($this->hasModel()) {
270 echo CHtml::activeHiddenField($this->model, $this->attribute, $this->htmlOptions);
271 } else {
272 echo CHtml::hiddenField($name, $this->value, $this->htmlOptions);
273 }
274
275 echo '<div id="slider_' . $id . '"></div>';
276 }
277
278 /**
279 * Registers required files and initialization script
280 */
281 public function registerClientScript()
282 {
283 /* publish assets dir */
284 $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets';
285 $assetsUrl = $this->getAssetsUrl($path);
286 $id = TbArray::getValue('id', $this->htmlOptions, $this->getId());
287
288 $jsFile = !empty($this->scales)
289 ? 'jQAllRangeSliders-withRuler-min.js'
290 : 'jQAllRangeSliders-min.js';
291
292 /* @var $cs CClientScript */
293 $cs = Yii::app()->getClientScript();
294
295 $cs->registerCoreScript('jquery');
296 $cs->registerCoreScript('jquery.ui');
297 $this->getYiiWheels()->registerAssetJs('jquery.mousewheel.min.js', CClientScript::POS_HEAD);
298
299 $cs->registerCssFile($assetsUrl . '/css/' . $this->theme . '.css');
300 $cs->registerScriptFile($assetsUrl . '/js/' . $jsFile);
301
302 $options = !empty($this->options) ? CJavaScript::encode($this->options) : '';
303
304 $inputValSet = "$('#{$id}').val(data.values.min+','+data.values.max);";
305
306 //inserting trigger
307 if (isset($this->events['valuesChanged'])) {
308 $orig = $this->events['valuesChanged'];
309 if (strpos($orig, 'js:') === 0) {
310 $orig = substr($orig, 3);
311 }
312 $orig = "\n($orig).apply(this, arguments);";
313 } else {
314 $orig = '';
315 }
316 $this->events['valuesChanged'] = "js: function(id, data) {
317 $inputValSet $orig
318 }";
319
320 ob_start();
321 echo "jQuery('#slider_{$id}').{$this->type}Slider({$options})";
322 foreach ($this->events as $event => $handler) {
323 echo ".on('{$event}', " . CJavaScript::encode($handler) . ")";
324 }
325
326 $cs->registerScript(__CLASS__ . '#' . $this->getId(), ob_get_clean() . ';');
327 }
328
329 /**
330 * Builds the options
331 */
332 protected function buildOptions()
333 {
334 $options = array(
335 'arrows' => $this->arrows,
336 'delayOut' => $this->delayOut,
337 'durationIn' => $this->durationIn,
338 'durationOut' => $this->durationOut,
339 'valueLabels' => $this->valueLabels,
340 'formatter' => $this->formatter,
341 'step' => $this->step,
342 'wheelMode' => $this->wheelMode,
343 'wheelSpeed' => $this->wheelSpeed,
344 'scales' => $this->scales,
345 'type' => ($this->type == 'dateRange' ? null : $this->inputType)
346 );
347 $this->options = array_filter($options);
348
349 if ($this->minRange && $this->maxRange && $this->minRange < $this->maxRange) {
350 $this->options = CMap::mergeArray(
351 $this->options,
352 array('range' => array('min' => $this->minRange, 'max' => $this->maxRange))
353 );
354 }
355 if ($this->minValue && $this->maxValue && $this->minValue < $this->maxValue) {
356 $this->options = CMap::mergeArray(
357 $this->options,
358 array('bounds' => array('min' => $this->minValue, 'max' => $this->maxValue))
359 );
360 }
361 if ($this->minDefaultValue && $this->maxDefaultValue && $this->minDefaultValue < $this->maxDefaultValue) {
362 $this->options = CMap::mergeArray(
363 $this->options,
364 array(
365 'defaultValues' => array(
366 'min' => $this->minDefaultValue,
367 'max' => $this->maxDefaultValue
368 )
369 )
370 );
371 }
372 if(!empty($this->pluginOptions))
373 {
374 $this->options = CMap::mergeArray($this->pluginOptions, $this->options);
375 }
376 }
377
378 /**
379 * Checks whether the option set is supported by the plugin
380 *
381 * @param mixed $attribute attribute
382 * @param array $availableOptions the possible values
383 * @param string $name the name of the attribute
384 *
385 * @throws CException
386 */
387 protected function checkOptionAttribute($attribute, $availableOptions, $name)
388 {
389 if (!in_array($attribute, $availableOptions)) {
390 throw new CException(Yii::t(
391 'zii',
392 'Unsupported "{attribute}" setting.',
393 array('{attribute}' => $name)
394 ));
395 }
396 }
397 }