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`, `shown`, or only shown when moving.
127 * Possible values are: show, hide and change.
128 */
129 public $valueLabels = 'shown';
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 the options to pass to the jQSlider
225 */
226 protected $options;
227
228 /**
229 * Widget's initialization
230 */
231 public function init()
232 {
233
234 $this->checkOptionAttribute($this->type, array('range', 'editRange', 'dateRange'), 'type');
235
236 $this->checkOptionAttribute($this->inputType, array('text', 'number'), 'inputType');
237
238 $this->checkOptionAttribute($this->valueLabels, array('shown', 'hidden'), 'valueLabels');
239
240 $this->checkOptionAttribute($this->theme, array('iThing', 'classic'), 'theme');
241
242 if($this->wheelMode)
243 {
244 $this->checkOptionAttribute($this->wheelMode, array('zoom', 'scroll'), 'wheelMode');
245 }
246 $this->attachBehavior('ywplugin', array('class' => 'yiiwheels.behaviors.WhPlugin'));
247 $this->buildOptions();
248 }
249
250 /**
251 * Widget's run method
252 */
253 public function run()
254 {
255 $this->renderField();
256 $this->registerClientScript();
257 }
258
259 /**
260 * Renders field and tag
261 */
262 public function renderField()
263 {
264 list($name, $id) = $this->resolveNameID();
265
266 if ($this->hasModel()) {
267 echo CHtml::activeHiddenField($this->model, $this->attribute, $this->htmlOptions);
268 } else {
269 echo CHtml::hiddenField($name, $this->value, $this->htmlOptions);
270 }
271
272 echo '<div id="slider_' . $id . '"></div>';
273 }
274
275 /**
276 * Registers required files and initialization script
277 */
278 public function registerClientScript()
279 {
280 /* publish assets dir */
281 $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets';
282 $assetsUrl = $this->getAssetsUrl($path);
283 $id = TbArray::getValue('id', $this->htmlOptions, $this->getId());
284
285 $jsFile = !empty($this->scales)
286 ? 'jQAllRangeSliders-withRuler-min.js'
287 : 'jQAllRangeSliders-min.js';
288
289 /* @var $cs CClientScript */
290 $cs = Yii::app()->getClientScript();
291
292 $cs->registerCoreScript('jquery');
293 $cs->registerCoreScript('jquery.ui');
294 $this->getYiiWheels()->registerAssetJs('jquery.mousewheel.min.js', CClientScript::POS_HEAD);
295
296 $cs->registerCssFile($assetsUrl . '/css/' . $this->theme . '.css');
297 $cs->registerScriptFile($assetsUrl . '/js/' . $jsFile);
298
299 $options = !empty($this->options) ? CJavaScript::encode($this->options) : '';
300
301 $inputValSet = "$('#{$id}').val(data.values.min+','+data.values.max);";
302
303 //inserting trigger
304 if (isset($this->events['valuesChanged'])) {
305 $orig = $this->events['valuesChanged'];
306 if (strpos($orig, 'js:') === 0) {
307 $orig = substr($orig, 3);
308 }
309 $orig = "\n($orig).apply(this, arguments);";
310 } else {
311 $orig = '';
312 }
313 $this->events['valuesChanged'] = "js: function(id, data) {
314 $inputValSet $orig
315 }";
316
317 ob_start();
318 echo "jQuery('#slider_{$id}').{$this->type}Slider({$options})";
319 foreach ($this->events as $event => $handler) {
320 echo ".on('{$event}', " . CJavaScript::encode($handler) . ")";
321 }
322
323 $cs->registerScript(__CLASS__ . '#' . $this->getId(), ob_get_clean() . ';');
324 }
325
326 /**
327 * Builds the options
328 */
329 protected function buildOptions()
330 {
331 $options = array(
332 'arrows' => $this->arrows,
333 'delayOut' => $this->delayOut,
334 'durationIn' => $this->durationIn,
335 'durationOut' => $this->durationOut,
336 'valueLabels' => $this->valueLabels,
337 'formatter' => $this->formatter,
338 'step' => $this->step,
339 'wheelMode' => $this->wheelMode,
340 'wheelSpeed' => $this->wheelSpeed,
341 'type' => ($this->type == 'dateRange' ? null : $this->inputType)
342 );
343 $this->options = array_filter($options);
344
345 if ($this->minRange && $this->maxRange && $this->minRange < $this->maxRange) {
346 $this->options = CMap::mergeArray(
347 $this->options,
348 array('range' => array('min' => $this->minRange, 'max' => $this->maxRange))
349 );
350 }
351 if ($this->minValue && $this->maxValue && $this->minValue < $this->maxValue) {
352 $this->options = CMap::mergeArray(
353 $this->options,
354 array('bounds' => array('min' => $this->minValue, 'max' => $this->maxValue))
355 );
356 }
357 if ($this->minDefaultValue && $this->maxDefaultValue && $this->minDefaultValue < $this->maxDefaultValue) {
358 $this->options = CMap::mergeArray(
359 $this->options,
360 array(
361 'defaultValues' => array(
362 'min' => $this->minDefaultValue,
363 'max' => $this->maxDefaultValue
364 )
365 )
366 );
367 }
368 }
369
370 /**
371 * Checks whether the option set is supported by the plugin
372 * @param mixed $attribute attribute
373 * @param array $availableOptions the possible values
374 * @param string $name the name of the attribute
375 * @throws CException
376 */
377 protected function checkOptionAttribute($attribute, $availableOptions, $name)
378 {
379 if(!in_array($attribute, $availableOptions))
380 {
381 throw new CException(Yii::t(
382 'zii',
383 'Unsupported "{attribute}" setting.',
384 array('{attribute}' => $name)
385 ));
386 }
387 }
388 }