YiiWheels
  • Package
  • Class
  • Tree

Packages

  • yiiwheels
    • behaviors
    • widgets
    • widgets
      • ace
      • box
      • datepicker
      • daterangepicker
      • datetimepicker
      • detail
      • editable
      • fileupload
      • fileuploader
      • formhelpers
      • gallery
      • google
      • grid
        • behaviors
        • operations
      • highcharts
      • maskInput
      • maskmoney
      • modal
      • multiselect
      • rangeslider
      • redactor
      • select2
      • sparklines
      • switch
      • timeago
      • timepicker
      • toggle
      • typeahead

Classes

  • WhEditable
  • WhEditableColumn
  • WhEditableDetailView
  • WhEditableField
  • WhEditableSaver
  1 <?php
  2 /**
  3  * WhEditable class
  4  *
  5  * Creates editable element on page (without linking to model attribute)
  6  *
  7  * @author Antonio Ramirez <amigo.cobos@gmail.com>
  8  * @copyright Copyright &copy; 2amigos.us 2013-
  9  * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
 10  * @package YiiWheels.widgets.editable
 11  *
 12  * @author Vitaliy Potapov <noginsk@rambler.ru>
 13  * @link https://github.com/vitalets/x-editable-yii
 14  * @copyright Copyright &copy; Vitaliy Potapov 2012
 15  * @version 1.3.1
 16  */
 17 class WhEditable extends CWidget
 18 {
 19 
 20     /**
 21      * @var string type of editable widget. Can be `text`, `textarea`, `select`, `date`, `checklist`, etc.
 22      * @see x-editable
 23      */
 24     public $type = null;
 25 
 26     /**
 27      * @var string url to submit value. Can be string or array containing Yii route, e.g. `array('site/updateUser')`
 28      * @see x-editable
 29      */
 30     public $url = null;
 31 
 32     /**
 33      * @var mixed primary key
 34      * @see x-editable
 35      */
 36     public $pk = null;
 37 
 38     /**
 39      * @var string name of field
 40      * @see x-editable
 41      */
 42     public $name = null;
 43 
 44     /**
 45      * @var array additional params to send on server
 46      * @see x-editable
 47      */
 48     public $params = null;
 49     /**
 50      * @var string css class of input. If `null` - default X-editable value is used: `input-medium`
 51      * @see x-editable
 52      */
 53     public $inputclass = null;
 54 
 55     /**
 56      * @var string mode of input: `inline` | `popup`. If not set - default X-editable value is used: `popup`.
 57      * @see x-editable
 58      */
 59     public $mode = null;
 60 
 61     /**
 62      * @var string text to be shown as element content
 63      */
 64     public $text = null;
 65 
 66     /**
 67      * @var mixed initial value. If not set - will be taken from text
 68      * @see x-editable
 69      */
 70     public $value = null;
 71 
 72     /**
 73      * @var string placement of popup. Can be `left`, `top`, `right`, `bottom`. If `null` - default X-editable value is used: `top`
 74      * @see x-editable
 75      */
 76     public $placement = null;
 77 
 78     /**
 79      * @var string text shown on empty field. If `null` - default X-editable value is used: `Empty`
 80      * @see x-editable
 81      */
 82     public $emptytext = null;
 83 
 84     /**
 85      * @var string visibility of buttons. Can be boolean `false|true` or string `bottom`.
 86      * @see x-editable
 87      */
 88     public $showbuttons = null;
 89 
 90     /**
 91      * @var string Strategy for sending data on server. Can be `auto|always|never`.
 92      * When 'auto' data will be sent on server only if **pk** and **url** defined, otherwise new value will be stored locally.
 93      * @see x-editable
 94      */
 95     public $send = null;
 96 
 97     /**
 98      * @var boolean will editable be initially disabled. It means editable plugin will be applied to element,
 99      * but you should call `.editable('enable')` method to activate it.
100      * To totally disable applying 'editable' to element use **apply** option.
101      * @see x-editable
102      */
103     public $disabled = false;
104 
105     //list
106     /**
107      * @var mixed source data for **select**, **checklist**. Can be string (url) or array in format:
108      * array( array("value" => 1, "text" => "abc"), ...)
109      * @package list
110      * @see x-editable
111      */
112     public $source = null;
113 
114     //date
115     /**
116      * @var string format to send date on server. If `null` - default X-editable value is used: `yyyy-mm-dd`.
117      * @package date
118      * @see x-editable
119      */
120     public $format = null;
121 
122     /**
123      * @var string format to display date in element. If `null` - equals to **format** option.
124      * @package date
125      * @see x-editable
126      */
127     public $viewformat = null;
128 
129     /**
130      * @var string template for **combodate** input. For details see http://vitalets.github.com/x-editable/docs.html#combodate.
131      * @package combodate
132      * @see x-editable
133      */
134     public $template = null;
135 
136     /**
137      * @var array full config for **combodate** input. For details see http://vitalets.github.com/combodate/#docs
138      * @package combodate
139      * @see x-editable
140      */
141     public $combodate = null;
142 
143     /**
144      * @var string separator used to display tags.
145      * @package select2
146      * @see x-editable
147      */
148     public $viewseparator = null;
149 
150     /**
151      * @var array full config for **select2** input. For details see http://ivaynberg.github.com/select2
152      * @package select2
153      * @see x-editable
154      */
155     public $select2 = null;
156 
157     //methods
158     /**
159      * A javascript function that will be invoked to validate value.
160      * Example:
161      * <pre>
162      * 'validate' => 'js: function(value) {
163      *     if($.trim(value) == "") return "This field is required";
164      * }'
165      * </pre>
166      *
167      * @var string
168      * @package callback
169      * @see x-editable
170      * @example
171      */
172     public $validate = null;
173 
174     /**
175      * A javascript function that will be invoked to process successful server response.
176      * Example:
177      * <pre>
178      * 'success' => 'js: function(response, newValue) {
179      *     if(!response.success) return response.msg;
180      * }'
181      * </pre>
182      *
183      * @var string
184      * @package callback
185      * @see x-editable
186      */
187     public $success = null;
188 
189     /**
190      * A javascript function that will be invoked to custom display value.
191      * Example:
192      * <pre>
193      * 'display' => 'js: function(value, sourceData) {
194      *      var escapedValue = $("&lt;div&gt;").text(value).html();
195      *      $(this).html("&lt;b&gt;"+escapedValue+"&lt;/b&gt;");
196      * }'
197      * </pre>
198      *
199      * @var string
200      * @package callback
201      * @see x-editable
202      */
203     public $display = null;
204 
205     /**
206      * DOM id of target where afterAjaxUpdate handler will call
207      * live update of editable element
208      *
209      * @var string
210      */
211     public $liveTarget = null;
212     /**
213      * jQuery selector of elements to wich will be applied editable.
214      * Usefull in combination of `liveTarget` when you want to keep field(s) editble
215      * after ajaxUpdate
216      *
217      * @var string
218      */
219     public $liveSelector = null;
220 
221     // --- X-editable events ---
222     /**
223      * A javascript function that will be invoked when editable element is initialized
224      * @var string
225      * @package event
226      * @see x-editable
227      */
228     public $onInit;
229     /**
230      * A javascript function that will be invoked when editable form is shown
231      * Example:
232      * <pre>
233      * 'onShown' => 'js: function() {
234      *     var $tip = $(this).data("editableContainer").tip();
235      *     $tip.find("input").val("overwriting value of input.");
236      * }'
237      * </pre>
238      *
239      * @var string
240      * @package event
241      * @see x-editable
242      */
243     public $onShown;
244     /**
245      * A javascript function that will be invoked when new value is saved
246      * Example:
247      * <pre>
248      * 'onSave' => 'js: function(e, params) {
249      *     alert("Saved value: " + params.newValue);
250      * }'
251      * </pre>
252      *
253      * @var string
254      * @package event
255      * @see x-editable
256      */
257     public $onSave;
258     /**
259      * A javascript function that will be invoked when editable form is hidden
260      * Example:
261      * <pre>
262      * 'onHidden' => 'js: function(e, reason) {
263      *    if(reason === "save" || reason === "cancel") {
264      *        //auto-open next editable
265      *        $(this).closest("tr").next().find(".editable").editable("show");
266      *    }
267      * }'
268      * </pre>
269      *
270      * @var string
271      * @package event
272      * @see x-editable
273      */
274     public $onHidden;
275 
276     /**
277      * @var array all config options of x-editable. See full list <a href="http://vitalets.github.com/x-editable/docs.html#editable">here</a>.
278      */
279     public $options = array();
280 
281     /**
282      * @var array HTML options of element. In `EditableColumn` htmlOptions are PHP expressions
283      * so you can use `$data` to bind values to particular cell, e.g. `'data-categoryID' => '$data->categoryID'`.
284      */
285     public $htmlOptions = array();
286 
287     /**
288      * @var boolean whether to HTML encode text on output
289      */
290     public $encode = true;
291 
292     /**
293      * @var boolean whether to apply 'editable' js plugin to element.
294      * Only **safe** attributes become editable.
295      */
296     public $apply = null;
297 
298     /**
299      * @var string title of popup. If `null` - will be generated automatically from attribute label.
300      * Can have token {label} inside that will be replaced with actual attribute label.
301      */
302     public $title = null;
303 
304     /**
305      * @var string for jQuery UI only. The JUI theme name.
306      */
307     public $theme = 'base';
308 
309 
310     protected $_prepareToAutotext = false;
311 
312     /**
313      * initialization of widget
314      *
315      */
316     public function init()
317     {
318         parent::init();
319 
320         if (!$this->name) {
321             throw new CException('Parameter "name" should be provided for Editable widget');
322         }
323 
324         $this->attachBehavior('ywplugin', array('class' => 'yiiwheels.behaviors.WhPlugin'));
325         $this->_prepareToAutotext = self::isAutotext($this->options, $this->type);
326     }
327 
328     /**
329      * Builds html options
330      */
331     public function buildHtmlOptions()
332     {
333         //html options
334         $htmlOptions = array(
335             'href' => '#',
336             'rel' => $this->liveSelector ? $this->liveSelector : $this->getSelector(),
337         );
338 
339         //set data-pk
340         if ($this->pk !== null) {
341             $htmlOptions['data-pk'] = is_array($this->pk) ? CJSON::encode($this->pk) : $this->pk;
342         }
343 
344         //if input type assumes autotext (e.g. select) we define value directly in data-value
345         //and do not fill element contents
346         if ($this->_prepareToAutotext) {
347             //for date we use 'format' to put it into value (if text not defined)
348             if ($this->type == 'date') {
349                 //if date comes as object, format it to string
350                 if ($this->value instanceOf DateTime || is_long($this->value)) {
351                     /*
352                     * unfortunatly datepicker's format does not match Yii locale dateFormat,
353                     * we need replacements below to convert date correctly
354                     */
355                     $count = 0;
356                     $format = str_replace('MM', 'MMMM', $this->format, $count);
357                     if (!$count) $format = str_replace('M', 'MMM', $format, $count);
358                     if (!$count) $format = str_replace('m', 'M', $format);
359 
360                     if ($this->value instanceof DateTime) {
361                         $timestamp = $this->value->getTimestamp();
362                     } else {
363                         $timestamp = $this->value;
364                     }
365 
366                     $this->value = Yii::app()->dateFormatter->format($format, $timestamp);
367                 }
368             }
369 
370             if (is_scalar($this->value)) {
371                 $this->htmlOptions['data-value'] = $this->value;
372             }
373             //if not scalar, value will be added to js options instead of html options
374         }
375 
376         //merging options
377         $this->htmlOptions = CMap::mergeArray($this->htmlOptions, $htmlOptions);
378 
379         //convert arrays to json string, otherwise yii can not render it:
380         //"htmlspecialchars() expects parameter 1 to be string, array given"
381         foreach ($this->htmlOptions as $k => $v) {
382             $this->htmlOptions[$k] = is_array($v) ? CJSON::encode($v) : $v;
383         }
384     }
385 
386     /**
387      * Builds javascript options
388      */
389     public function buildJsOptions()
390     {
391         //normalize url from array
392         $this->url = CHtml::normalizeUrl($this->url);
393 
394         $options = array(
395             'name' => $this->name,
396             'title' => CHtml::encode($this->title),
397         );
398 
399         //if value needed for autotext and it's not scalar --> add it to js options
400         if ($this->_prepareToAutotext && !is_scalar($this->value)) {
401             $options['value'] = $this->value;
402         }
403 
404         //support of CSRF out of box, see https://github.com/vitalets/x-editable-yii/issues/38
405         if (Yii::app()->request->enableCsrfValidation) {
406             $csrfTokenName = Yii::app()->request->csrfTokenName;
407             $csrfToken = Yii::app()->request->csrfToken;
408             if (!isset($this->params[$csrfTokenName])) {
409                 $this->params[$csrfTokenName] = $csrfToken;
410             }
411         }
412 
413         //simple options set directly from config
414         foreach (array(
415                      'url',
416                      'type',
417                      'mode',
418                      'placement',
419                      'emptytext',
420                      'params',
421                      'inputclass',
422                      'format',
423                      'viewformat',
424                      'template',
425                      'combodate',
426                      'select2',
427                      'viewseparator',
428                      'showbuttons',
429                      'send',
430                  ) as $option) {
431             if ($this->$option !== null) {
432                 $options[$option] = $this->$option;
433             }
434         }
435 
436         if ($this->source) {
437 
438             //if source is array --> convert it to x-editable format.
439             //Since 1.1.0 source as array with one element is NOT treated as Yii route!
440             if (is_array($this->source)) {
441                 //if first elem is array assume it's normal x-editable format, so just pass it
442                 if (isset($this->source[0]) && is_array($this->source[0])) {
443                     $options['source'] = $this->source;
444                 } else { //else convert to x-editable source format {value: 1, text: 'abc'}
445                     $options['source'] = array();
446                     foreach ($this->source as $value => $text) {
447                         $options['source'][] = array('value' => $value, 'text' => $text);
448                     }
449                 }
450             } else { //source is url string (or js function)
451                 $options['source'] = CHtml::normalizeUrl($this->source);
452             }
453         }
454 
455         //callbacks
456         foreach (array('validate', 'success', 'display') as $method) {
457             if (isset($this->$method)) {
458                 $options[$method] = (strpos($this->$method, 'js:') !== 0 ? 'js:' : '') . $this->$method;
459             }
460         }
461 
462         //merging options
463         $this->options = CMap::mergeArray($this->options, $options);
464 
465         //i18n for `clear` in date and datetime
466         if ($this->type == 'date' || $this->type == 'datetime') {
467             if (!isset($this->options['clear'])) {
468                 $this->options['clear'] = Yii::t('EditableField.editable', 'x clear');
469             }
470         }
471     }
472 
473     /**
474      * Registers client script
475      * @return string
476      */
477     public function registerClientScript()
478     {
479         $selector = "a[rel=\"{$this->htmlOptions['rel']}\"]";
480         if ($this->liveTarget) {
481             $selector = '#' . $this->liveTarget . ' ' . $selector;
482         }
483         $script = "$('" . $selector . "')";
484 
485         //attach events
486         foreach (array('init', 'shown', 'save', 'hidden') as $event) {
487             $eventName = 'on' . ucfirst($event);
488             if (isset($this->$eventName)) {
489                 // CJavaScriptExpression appeared only in 1.1.11, will turn to it later
490                 //$event = ($this->onInit instanceof CJavaScriptExpression) ? $this->onInit : new CJavaScriptExpression($this->onInit);
491                 $eventJs = (strpos($this->$eventName, 'js:') !== 0 ? 'js:' : '') . $this->$eventName;
492                 $script .= "\n.on('" . $event . "', " . CJavaScript::encode($eventJs) . ")";
493             }
494         }
495 
496         //apply editable
497         $options = CJavaScript::encode($this->options);
498         $script .= ".editable($options);";
499 
500         //wrap in anonymous function for live update
501         if ($this->liveTarget) {
502             $script .= "\n $('body').on('ajaxUpdate.editable', function(e){ if(e.target.id == '" . $this->liveTarget . "'){yiiEditable();}});";
503             $script = "(function yiiEditable() {\n " . $script . "\n}());";
504         }
505 
506         Yii::app()->getClientScript()->registerScript(__CLASS__ . '-' . $selector, $script);
507 
508         return $script;
509     }
510 
511     /**
512      * Registers assets
513      * @throws CException
514      */
515     public function registerAssets()
516     {
517         $cs = Yii::app()->getClientScript();
518 
519 
520         $path = __DIR__ . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'bootstrap-editable';
521         $assetsUrl = $this->getAssetsUrl($path);
522 
523         //register assets
524         $cs->registerCssFile($assetsUrl . '/css/bootstrap-editable.css');
525         $cs->registerScriptFile($assetsUrl . '/js/bootstrap-editable.js', CClientScript::POS_END);
526 
527         //include moment.js for combodate
528         if ($this->type == 'combodate') {
529             $path = __DIR__ . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'moment';
530             $momentUrl = Yii::app()->assetManager->publish($path, false, -1, $this->getApi()->forceCopyAssets);
531             $cs->registerScriptFile($momentUrl . '/moment.min.js');
532         }
533 
534         //include select2 lib for select2 type
535         if ($this->type == 'select2') {
536             $path = __DIR__ . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'select2';
537             $select2Url = Yii::app()->assetManager->publish($path, false, -1, $this->getApi()->forceCopyAssets);
538             $cs->registerScriptFile($select2Url . '/select2.min.js');
539             $cs->registerCssFile($select2Url . '/select2.css');
540         }
541 
542         //include bootstrap-datetimepicker
543         if ($this->type == 'datetime') {
544             $path = __DIR__ . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'bootstrap-datetimepicker';
545             $dateTimePickerUrl = Yii::app()->assetManager->publish($path, false, -1, $this->getApi()->forceCopyAssets);
546             $cs->registerScriptFile($dateTimePickerUrl . '/js/bootstrap-datetimepicker.js');
547             $cs->registerCssFile($dateTimePickerUrl . '/css/datetimepicker.css');
548         }
549     }
550 
551     /**
552      * Widget run method
553      */
554     public function run()
555     {
556         //Register script (even if apply = false to support live update)
557         if ($this->apply !== false || $this->liveTarget) {
558             $this->buildHtmlOptions();
559             $this->buildJsOptions();
560             $this->registerAssets();
561             $this->registerClientScript();
562         }
563 
564         if ($this->apply !== false) {
565             $this->renderLink();
566         } else {
567             $this->renderText();
568         }
569     }
570 
571     /**
572      * Renders a link
573      */
574     public function renderLink()
575     {
576         echo CHtml::openTag('a', $this->htmlOptions);
577         $this->renderText();
578         echo CHtml::closeTag('a');
579     }
580 
581     /**
582      * Renders text
583      */
584     public function renderText()
585     {
586         $encodedText = $this->encode ? CHtml::encode($this->text) : $this->text;
587         if ($this->type == 'textarea') {
588             $encodedText = preg_replace('/\r?\n/', '<br>', $encodedText);
589         }
590         echo $encodedText;
591     }
592 
593     /**
594      * Returns the plugin selector
595      * @return null|string
596      */
597     public function getSelector()
598     {
599         //for live updates selector should not contain pk
600         if ($this->liveTarget) {
601             return $this->name;
602         }
603 
604         $pk = $this->pk;
605         if ($pk === null) {
606             $pk = 'new';
607         } else {
608             //support of composite keys: convert to string: e.g. 'id-1_lang-ru'
609             if (is_array($pk)) {
610                 //below not works in PHP < 5.3, see https://github.com/vitalets/x-editable-yii/issues/39
611                 //$pk = join('_', array_map(function($k, $v) { return $k.'-'.$v; }, array_keys($pk), $pk));
612                 $buffer = array();
613                 foreach ($pk as $k => $v) {
614                     $buffer[] = $k . '-' . $v;
615                 }
616                 $pk = join('_', $buffer);
617             }
618         }
619 
620 
621         return $this->name . '_' . $pk;
622     }
623 
624     /**
625      * Returns is autotext should be applied to widget:
626      * e.g. for 'select' to display text for id
627      *
628      * @param mixed $options
629      * @param mixed $type
630      * @return bool
631      */
632     public static function isAutotext($options, $type)
633     {
634         return (!isset($options['autotext']) || $options['autotext'] !== 'never')
635         && in_array($type, array(
636             'select',
637             'checklist',
638             'date',
639             'datetime',
640             'dateui',
641             'combodate',
642             'select2'
643         ));
644     }
645 
646     /**
647      * Returns php-array as valid x-editable source in format:
648      * [{value: 1, text: 'text1'}, {...}]
649      *
650      * See https://github.com/vitalets/x-editable-yii/issues/37
651      *
652      * @param mixed $models
653      * @param mixed $valueField
654      * @param mixed $textField
655      * @param mixed $groupField
656      * @param mixed $groupTextField
657      * @return array
658      */
659     public static function source($models, $valueField = '', $textField = '', $groupField = '', $groupTextField = '')
660     {
661         $listData = array();
662 
663         $first = reset($models);
664 
665         //simple 1-dimensional array: 0 => 'text 0', 1 => 'text 1'
666         if ($first && (is_string($first) || is_numeric($first))) {
667             foreach ($models as $key => $text) {
668                 $listData[] = array('value' => $key, 'text' => $text);
669             }
670             return $listData;
671         }
672 
673         // 2-dimensional array or dataset
674         if ($groupField === '') {
675             foreach ($models as $model) {
676                 $value = CHtml::value($model, $valueField);
677                 $text = CHtml::value($model, $textField);
678                 $listData[] = array('value' => $value, 'text' => $text);
679             }
680         } else {
681             if (!$groupTextField) {
682                 $groupTextField = $groupField;
683             }
684             $groups = array();
685             foreach ($models as $model) {
686                 $group = CHtml::value($model, $groupField);
687                 $groupText = CHtml::value($model, $groupTextField);
688                 $value = CHtml::value($model, $valueField);
689                 $text = CHtml::value($model, $textField);
690                 if ($group === null) {
691                     $listData[] = array('value' => $value, 'text' => $text);
692                 } else {
693                     if (!isset($groups[$group])) {
694                         $groups[$group] = array(
695                             'value' => $group,
696                             'text' => $groupText,
697                             'children' => array(),
698                             'index' => count($listData)
699                         );
700                         $listData[] = 'group'; //placeholder, will be replaced in future
701                     }
702                     $groups[$group]['children'][] = array('value' => $value, 'text' => $text);
703                 }
704             }
705 
706             //fill placeholders with group data
707             foreach ($groups as $group) {
708                 $index = $group['index'];
709                 unset($group['index']);
710                 $listData[$index] = $group;
711             }
712         }
713 
714         return $listData;
715     }
716 
717     /**
718      * injects ajaxUpdate event into widget
719      *
720      * @param mixed $widget
721      */
722     public static function attachAjaxUpdateEvent($widget)
723     {
724         $trigger = '$("#' . $widget->id . '").trigger("ajaxUpdate.editable");';
725 
726         //check if trigger already inserted by another column
727         if (strpos($widget->afterAjaxUpdate, $trigger) !== false) return;
728 
729         //inserting trigger
730         if (strlen($widget->afterAjaxUpdate)) {
731             $orig = $widget->afterAjaxUpdate;
732             if (strpos($orig, 'js:') === 0) $orig = substr($orig, 3);
733             $orig = "\n($orig).apply(this, arguments);";
734         } else {
735             $orig = '';
736         }
737         $widget->afterAjaxUpdate = "js: function(id, data) {
738             $trigger $orig
739         }";
740 
741         $widget->registerClientScript();
742     }
743 
744 }
745 
YiiWheels API documentation generated by ApiGen 2.8.0