HCE Project DC service web UI  0.2
Hierarchical Cluster Engine DC service web UI
 All Classes Namespaces Files Functions Variables Pages
TbEditable.php
Go to the documentation of this file.
1 <?php
17 class TbEditable extends CWidget
18 {
19  //note: only most usefull options are on first level of config.
20 
21  // --- start of X-editable options ----
26  public $type = null;
31  public $url = null;
36  public $pk = null;
41  public $name = null;
46  public $params = null;
51  public $inputclass = null;
56  public $mode = null;
60  public $text = null;
65  public $value = null;
70  public $placement = null;
71 
76  public $emptytext = null;
77 
82  public $showbuttons = null;
83 
89  public $send = null;
90 
97  public $disabled = false;
98 
99  //list
106  public $source = null;
107 
108  //date
114  public $format = null;
120  public $viewformat = null;
126  public $template = null;
132  public $combodate = null;
138  public $viewseparator = null;
144  public $select2 = null;
145 
146  //methods
161  public $validate = null;
175  public $success = null;
190  public $display = null;
191 
198  public $liveTarget = null;
206  public $liveSelector = null;
207 
208  // --- X-editable events ---
215  public $onInit;
230  public $onShown;
244  public $onSave;
261  public $onHidden;
262 
266  public $options = array();
267 
272  public $htmlOptions = array();
273 
277  public $encode = true;
278 
283  public $apply = null;
284 
289  public $title = null;
290 
291  //themeUrl, theme and cssFile copied from CJuiWidget to allow include custom theme for jQuery UI
296  public $themeUrl;
300  public $theme='base';
304  public $cssFile='jquery-ui.css';
305 
306  protected $_prepareToAutotext = false;
307 
312  public function init()
313  {
314  parent::init();
315 
316  if (!$this->name) {
317  throw new CException('Parameter "name" should be provided for TbEditable widget');
318  }
319 
320  /*
321  If set this flag to true --> element content will stay empty
322  and value will be rendered to data-value attribute to apply autotext afterwards.
323  */
324  $this->_prepareToAutotext = self::isAutotext($this->options, $this->type);
325 
326  /*
327  For `date` and `datetime` we need format to be on php side to make conversions.
328  But we can not set default format as datepicker and combodate has different formats.
329  So do it here:
330  */
331  if (!$this->format && $this->type == 'date') {
332  $this->format = 'yyyy-mm-dd';
333  }
334  if (!$this->format && $this->type == 'datetime') {
335  $this->format = 'yyyy-mm-dd hh:ii:ss';
336  }
337  }
338 
339  public function buildHtmlOptions()
340  {
341  //html options
342  $htmlOptions = array(
343  'href' => '#',
344  'rel' => $this->liveSelector ? $this->liveSelector : $this->getSelector(),
345  );
346 
347  //set data-pk
348  if($this->pk !== null) {
349  $htmlOptions['data-pk'] = is_array($this->pk) ? CJSON::encode($this->pk) : $this->pk;
350  }
351 
352  //if input type assumes autotext (e.g. select) we define value directly in data-value
353  //and do not fill element contents
354  if ($this->_prepareToAutotext) {
355  //for date we use 'format' to put it into value (if text not defined)
356  if ($this->type == 'date' || $this->type == 'datetime') {
357  //if date comes as object OR timestamp, format it to string
358  if($this->value instanceOf DateTime || is_long($this->value) || (is_string($this->value) && ctype_digit($this->value))) {
359  /*
360  * unfortunatly bootstrap datepicker's format does not match
361  * Yii locale dateFormat, we need replacements below to convert
362  * date correctly.
363  *
364  * Yii format:
365  * http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
366  *
367  * Datepicker format:
368  * https://github.com/eternicode/bootstrap-datepicker#format
369  *
370  * Datetimepicker format:
371  * https://github.com/smalot/bootstrap-datetimepicker#format
372  */
373  //months: M --> MMM, m --> M
374  $count = 0;
375  $format = str_replace('MM', 'MMMM', $this->format, $count);
376  if(!$count) $format = str_replace('M', 'MMM', $format, $count);
377  if(!$count) $format = str_replace('m', 'M', $format);
378  if($this->type == 'datetime') {
379  //minutes: i --> m
380  $format = str_replace('i', 'm', $format);
381  //hours: h --> H, H --> h
382  $count = 0;
383  $format = str_replace('h', 'H', $format, $count);
384  if(!$count) {
385  $format = str_replace('H', 'h', $format);
386  }
387  }
388 
389  if($this->value instanceof DateTime) {
390  $timestamp = $this->value->getTimestamp();
391  } else {
392  $timestamp = $this->value;
393  }
394 
395  $this->value = Yii::app()->dateFormatter->format($format, $timestamp);
396  }
397  }
398 
399  if(is_scalar($this->value)) {
400  $this->htmlOptions['data-value'] = $this->value;
401  }
402  //if not scalar, value will be added to js options instead of html options
403  }
404 
405  //merging options
406  $this->htmlOptions = CMap::mergeArray($this->htmlOptions, $htmlOptions);
407 
408  //convert arrays to json string, otherwise yii can not render it:
409  //"htmlspecialchars() expects parameter 1 to be string, array given"
410  foreach($this->htmlOptions as $k => $v) {
411  $this->htmlOptions[$k] = is_array($v) ? CJSON::encode($v) : $v;
412  }
413  }
414 
415  public function buildJsOptions()
416  {
417  //normalize url from array
418  $this->url = CHtml::normalizeUrl($this->url);
419 
420  $options = array(
421  'name' => $this->name,
422  'title' => CHtml::encode($this->title),
423  );
424 
425  //if value needed for autotext and it's not scalar --> add it to js options
426  if ($this->_prepareToAutotext && !is_scalar($this->value)) {
427  $options['value'] = $this->value;
428  }
429 
430  //support of CSRF out of box, see https://github.com/vitalets/x-editable-yii/issues/38
431  if (Yii::app()->request->enableCsrfValidation) {
432  $csrfTokenName = Yii::app()->request->csrfTokenName;
433  $csrfToken = Yii::app()->request->csrfToken;
434  if(!isset($this->params[$csrfTokenName])) {
435  $this->params[$csrfTokenName] = $csrfToken;
436  }
437  }
438 
439  //simple options set directly from config
440  foreach(array(
441  'url',
442  'type',
443  'mode',
444  'placement',
445  'emptytext',
446  'params',
447  'inputclass',
448  'format',
449  'viewformat',
450  'template',
451  'combodate',
452  'select2',
453  'viewseparator',
454  'showbuttons',
455  'send',
456  ) as $option) {
457  if ($this->$option !== null) {
458  $options[$option] = $this->$option;
459  }
460  }
461 
462  if ($this->source) {
463  //if source is array --> convert it to x-editable format.
464  //Since 1.1.0 source as array with one element is NOT treated as Yii route!
465  if(is_array($this->source)) {
466  //if first elem is array assume it's normal x-editable format, so just pass it
467  if(isset($this->source[0]) && is_array($this->source[0])) {
468  $options['source'] = $this->source;
469  } else { //else convert to x-editable source format {value: 1, text: 'abc'}
470  $options['source'] = array();
471  foreach($this->source as $value => $text) {
472  $options['source'][] = array('value' => $value, 'text' => $text);
473  }
474  }
475  } else { //source is url string (or js function)
476  $options['source'] = CHtml::normalizeUrl($this->source);
477  }
478  }
479 
480  //callbacks
481  foreach(array('validate', 'success', 'display') as $method) {
482  if(isset($this->$method)) {
483  $options[$method]=(strpos($this->$method, 'js:') !== 0 ? 'js:' : '') . $this->$method;
484  }
485  }
486 
487  //merging options
488  $this->options = CMap::mergeArray($this->options, $options);
489 
490  //i18n for `clear` in date and datetime
491  if($this->type == 'date' || $this->type == 'datetime') {
492  if(!isset($this->options['clear'])) {
493  $this->options['clear'] = Yii::t('TbEditableField.editable', 'x clear');
494  }
495  }
496  }
497 
498  public function registerClientScript()
499  {
500  $selector = "a[rel=\"{$this->htmlOptions['rel']}\"]";
501  if($this->liveTarget) {
502  $selector = '#'.$this->liveTarget.' '.$selector;
503  }
504  $script = "$('".$selector."')";
505 
506  //attach events
507  foreach(array('init', 'shown', 'save', 'hidden') as $event) {
508  $eventName = 'on'.ucfirst($event);
509  if (isset($this->$eventName)) {
510  // CJavaScriptExpression appeared only in 1.1.11, will turn to it later
511  //$event = ($this->onInit instanceof CJavaScriptExpression) ? $this->onInit : new CJavaScriptExpression($this->onInit);
512  $eventJs = (strpos($this->$eventName, 'js:') !== 0 ? 'js:' : '') . $this->$eventName;
513  $script .= "\n.on('".$event."', ".CJavaScript::encode($eventJs).")";
514  }
515  }
516 
517  //apply editable
518  $options = CJavaScript::encode($this->options);
519  $script .= ".editable($options);";
520 
521  //wrap in anonymous function for live update
522  if($this->liveTarget) {
523  $script .= "\n $('body').on('ajaxUpdate.editable', function(e){ if(e.target.id == '".$this->liveTarget."') yiiEditable(); });";
524  $script = "(function yiiEditable() {\n ".$script."\n}());";
525  }
526 
527  Yii::app()->getClientScript()->registerScript(__CLASS__ . '-' . $selector, $script);
528 
529  return $script;
530  }
531 
532  public function registerAssets() {
533 
534  $booster = Booster::getBooster();
535 
536  if ($this->type == 'date' || $this->type == 'combodate') {
538  $widget = Yii::app()->widgetFactory->createWidget(
539  $this->getOwner(),
540  'booster.widgets.TbDatePicker',
541  array('options' => isset($this->options['datepicker']) ? $this->options['datepicker'] : array())
542  );
543  $widget->registerLanguageScript();
544  } elseif ($this->type == 'datetime') {
545  $booster->registerPackage('datetimepicker');
546 
548  $widget = Yii::app()->widgetFactory->createWidget(
549  $this->getOwner(),
550  'booster.widgets.TbDateTimePicker',
551  array('options' => $this->options['datetimepicker'])
552  );
553  $widget->registerLanguageScript();
554  }
555 
556  if ($this->type == 'combodate') { // include moment.js if needed
557  $booster->registerPackage('moment');
558  } elseif ($this->type == 'select2') { //include select2 if needed
559  $booster->registerPackage('select2');
560  }
561 
562  $booster->registerPackage('x-editable');
563 
564  return;
565 
566  /* TODO original */
567  $am = Yii::app()->getAssetManager();
568  $cs = Yii::app()->getClientScript();
569  $form = yii::app()->editable->form;
570  $mode = $this->mode ? $this->mode : yii::app()->editable->defaults['mode'];
571 
572  // bootstrap
573  if($form === EditableConfig::FORM_BOOTSTRAP) {
574  if (($bootstrap = yii::app()->getComponent('bootstrap'))) {
575  $bootstrap->registerCoreCss();
576  $bootstrap->registerCoreScripts();
577  } else {
578  throw new CException('You need to setup Yii-bootstrap extension first.');
579  }
580 
581  $assetsUrl = $am->publish(Yii::getPathOfAlias('editable.assets.bootstrap-editable'));
582  $js = 'bootstrap-editable.js';
583  $css = 'bootstrap-editable.css';
584  // jqueryui
585  } elseif($form === EditableConfig::FORM_JQUERYUI) {
586  if($mode === EditableConfig::POPUP && Yii::getVersion() < '1.1.13' ) {
587  throw new CException('jQuery UI editable popup supported from Yii 1.1.13+');
588  }
589 
590  //register jquery ui
591  $this->registerJQueryUI();
592 
593  $assetsUrl = $am->publish(Yii::getPathOfAlias('editable.assets.jqueryui-editable'));
594  $js = 'jqueryui-editable.js';
595  $css = 'jqueryui-editable.css';
596  // plain jQuery
597  } else {
598  $assetsUrl = $am->publish(Yii::getPathOfAlias('editable.assets.jquery-editable'));
599  $js = 'jquery-editable-poshytip.js';
600  $css = 'jquery-editable.css';
601 
602  //publish & register poshytip for popup version
603  if($mode === EditableConfig::POPUP) {
604  $poshytipUrl = $am->publish(Yii::getPathOfAlias('editable.assets.poshytip'));
605  $cs->registerScriptFile($poshytipUrl . '/jquery.poshytip.js');
606  $cs->registerCssFile($poshytipUrl . '/tip-yellowsimple/tip-yellowsimple.css');
607  }
608 
609  //register jquery ui for datepicker
610  if($this->type == 'date' || $this->type == 'dateui') {
611  $this->registerJQueryUI();
612  }
613  }
614 
615  //register assets
616  $cs->registerCssFile($assetsUrl.'/css/'.$css);
617  $cs->registerScriptFile($assetsUrl.'/js/'.$js, CClientScript::POS_END);
618 
619  //include moment.js for combodate
620  if($this->type == 'combodate') {
621  $momentUrl = $am->publish(Yii::getPathOfAlias('editable.assets.moment'));
622  $cs->registerScriptFile($momentUrl.'/moment.min.js');
623  }
624 
625  //include select2 lib for select2 type
626  if($this->type == 'select2') {
627  $select2Url = $am->publish(Yii::getPathOfAlias('editable.assets.select2'));
628  $cs->registerScriptFile($select2Url.'/select2.min.js');
629  $cs->registerCssFile($select2Url.'/select2.css');
630  }
631 
632  //include bootstrap-datetimepicker
633  if($this->type == 'datetime') {
634  $url = $am->publish(Yii::getPathOfAlias('editable.assets.bootstrap-datetimepicker'));
635  $cs->registerScriptFile($url.'/js/bootstrap-datetimepicker.js');
636  $cs->registerCssFile($url.'/css/datetimepicker.css');
637  }
638 
639  //TODO: include locale for datepicker
640  //may be do it manually?
641  /*
642  if ($this->type == 'date' && $this->language && substr($this->language, 0, 2) != 'en') {
643  //todo: check compare dp locale name with yii's
644  $localesUrl = Yii::app()->getAssetManager()->publish(Yii::getPathOfAlias('ext.editable.assets.js.locales'));
645  Yii::app()->clientScript->registerScriptFile($localesUrl . '/bootstrap-datepicker.'. str_replace('_', '-', $this->language).'.js', CClientScript::POS_END);
646  }
647  */
648  }
649 
650  public function run()
651  {
652  //Register script (even if apply = false to support live update)
653  if($this->apply !== false || $this->liveTarget) {
654  $this->buildHtmlOptions();
655  $this->buildJsOptions();
656  $this->registerAssets();
657  $this->registerClientScript();
658  }
659 
660  if($this->apply !== false) {
661  $this->renderLink();
662  } else {
663  $this->renderText();
664  }
665  }
666 
667  public function renderLink()
668  {
669  echo CHtml::openTag('a', $this->htmlOptions);
670  $this->renderText();
671  echo CHtml::closeTag('a');
672  }
673 
674  public function renderText()
675  {
676  echo $this->encode ? CHtml::encode($this->text) : $this->text;
677  }
678 
679  public function getSelector()
680  {
681  //for live updates selector should not contain pk
682  if($this->liveTarget) {
683  return $this->name;
684  }
685 
686  $pk = $this->pk;
687  if($pk === null) {
688  $pk = 'new';
689  } else {
690  //support of composite keys: convert to string: e.g. 'id-1_lang-ru'
691  if(is_array($pk)) {
692  //below not works in PHP < 5.3, see https://github.com/vitalets/x-editable-yii/issues/39
693  //$pk = join('_', array_map(function($k, $v) { return $k.'-'.$v; }, array_keys($pk), $pk));
694  $buffer = array();
695  foreach($pk as $k => $v) {
696  $buffer[] = $k.'-'.$v;
697  }
698  $pk = join('_', $buffer);
699  }
700  }
701 
702 
703  return $this->name.'_'.$pk;
704  }
705 
713  public static function isAutotext($options, $type)
714  {
715  return (!isset($options['autotext']) || $options['autotext'] !== 'never')
716  && in_array($type, array(
717  'select',
718  'checklist',
719  'date',
720  'datetime',
721  'dateui',
722  'combodate',
723  'select2'
724  ));
725  }
726 
739  public static function source($models, $valueField='', $textField='', $groupField='', $groupTextField='')
740  {
741  $listData=array();
742 
743  $first = reset($models);
744 
745  //simple 1-dimensional array: 0 => 'text 0', 1 => 'text 1'
746  if($first && (is_string($first) || is_numeric($first))) {
747  foreach($models as $key => $text) {
748  $listData[] = array('value' => $key, 'text' => $text);
749  }
750  return $listData;
751  }
752 
753  // 2-dimensional array or dataset
754  if($groupField === '') {
755  foreach($models as $model) {
756  $value = CHtml::value($model, $valueField);
757  $text = CHtml::value($model, $textField);
758  $listData[] = array('value' => $value, 'text' => $text);
759  }
760  } else {
761  if(!$groupTextField) {
762  $groupTextField = $groupField;
763  }
764  $groups = array();
765  foreach($models as $model) {
766  $group=CHtml::value($model,$groupField);
767  $groupText=CHtml::value($model,$groupTextField);
768  $value=CHtml::value($model,$valueField);
769  $text=CHtml::value($model,$textField);
770  if($group === null) {
771  $listData[] = array('value' => $value, 'text' => $text);
772  } else {
773  if(!isset($groups[$group])) {
774  $groups[$group] = array('value' => $group, 'text' => $groupText, 'children' => array(), 'index' => count($listData));
775  $listData[] = 'group'; //placeholder, will be replaced in future
776  }
777  $groups[$group]['children'][] = array('value' => $value, 'text' => $text);
778  }
779  }
780 
781  //fill placeholders with group data
782  foreach($groups as $group) {
783  $index = $group['index'];
784  unset($group['index']);
785  $listData[$index] = $group;
786  }
787  }
788 
789  return $listData;
790  }
791 
797  public static function attachAjaxUpdateEvent($widget)
798  {
799  $trigger = '$("#'.$widget->id.'").trigger("ajaxUpdate.editable");';
800 
801  //check if trigger already inserted by another column
802  if(strpos($widget->afterAjaxUpdate, $trigger) !== false) return;
803 
804  //inserting trigger
805  if(strlen($widget->afterAjaxUpdate)) {
806  $orig = $widget->afterAjaxUpdate;
807  if(strpos($orig, 'js:')===0) $orig = substr($orig,3);
808  $orig = "\n($orig).apply(this, arguments);";
809  } else {
810  $orig = '';
811  }
812  $widget->afterAjaxUpdate = "js: function(id, data) {
813  $trigger $orig
814  }";
815 
816  $widget->registerClientScript();
817  }
818 
819 
824  protected function registerJQueryUI()
825  {
826  $cs=Yii::app()->getClientScript();
827  if($this->themeUrl===null) {
828  $this->themeUrl=$cs->getCoreScriptUrl().'/jui/css';
829  }
830  $cs->registerCssFile($this->themeUrl.'/'.$this->theme.'/'.$this->cssFile);
831  $cs->registerPackage('jquery.ui');
832  }
833 }