Commits

Jacob Moen committed bca7caf

Editable extension - with 'assets' directory renamed to 'eassets'

  • Participants
  • Parent commits 4c42b6e

Comments (0)

Files changed (39)

protected/extensions/editable/EditableColumn.php

+<?php
+/**
+ * EditableColumn class file.
+ * 
+ * This widget makes editable column in GridView
+ * 
+ * @author Vitaliy Potapov <noginsk@rambler.ru>
+ * @link https://github.com/vitalets/yii-bootstrap-editable
+ * @copyright Copyright &copy; Vitaliy Potapov 2012
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version 1.0.0
+ */
+
+Yii::import('ext.editable.EditableField');
+Yii::import('zii.widgets.grid.CDataColumn');
+
+class EditableColumn extends CDataColumn
+{
+    //editable params
+    public $editable = array();
+
+    //flag to render client script only once
+    protected $isScriptRendered = false;
+
+    public function init()
+    {
+        if (!$this->grid->dataProvider instanceOf CActiveDataProvider) {
+            throw new CException('EditableColumn can be applied only to grid based on CActiveDataProvider');
+        }
+        if (!$this->name) {
+            throw new CException('You should provide name for EditableColumn');
+        }
+
+        parent::init();
+        
+        if($this->isEditable($this->grid->dataProvider->model)) {
+            $this->attachAjaxUpdateEvent();
+        }
+    }
+
+    protected function renderDataCellContent($row, $data)
+    {
+        if(!$this->isEditable($data)) {
+            parent::renderDataCellContent($row, $data);
+            return; 
+        }
+        
+        $options = CMap::mergeArray($this->editable, array(
+            'model'     => $data,
+            'attribute' => $this->name,
+        ));
+        
+        //if value defined for column --> use it as element text
+        if(strlen($this->value)) {
+            ob_start();
+            parent::renderDataCellContent($row, $data);
+            $text = ob_get_clean();
+            $options['text'] = $text;
+            $options['encode'] = false;
+        }
+       
+        $editable = $this->grid->controller->createWidget('EditableField', $options);
+
+        //manually make selector non unique to match all cells in column
+        $selector = get_class($editable->model) . '_' . $editable->attribute;
+        $editable->htmlOptions['rel'] = $selector;
+
+        $editable->renderLink();
+
+        //manually render client script (one for all cells in column)
+        if (!$this->isScriptRendered) {
+            $script = $editable->registerClientScript();
+            Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $selector.'-event', '
+                $("#'.$this->grid->id.'").parent().on("ajaxUpdate.yiiGridView", "#'.$this->grid->id.'", function() {'.$script.'});
+            ');
+            $this->isScriptRendered = true;
+        }
+    }
+    
+   /**
+   * Unfortunatly Yii yet does not support custom js events in it's widgets. 
+   * So we need to invoke it manually to ensure update of editables on grid ajax update.
+   * 
+   * issue in Yii github: https://github.com/yiisoft/yii/issues/1313
+   * 
+   */
+    protected function attachAjaxUpdateEvent()
+    {
+        $trigger = '$("#"+id).trigger("ajaxUpdate");';
+        
+        //check if trigger already inserted by another column
+        if(strpos($this->grid->afterAjaxUpdate, $trigger) !== false) return;
+        
+        //inserting trigger
+        if(strlen($this->grid->afterAjaxUpdate)) {
+            $orig = $this->grid->afterAjaxUpdate;
+            if(strpos($orig, 'js:')===0) $orig = substr($orig,3);
+            $orig = "\n($orig).apply(this, arguments);";
+        } else {
+            $orig = '';
+        }
+        $this->grid->afterAjaxUpdate = "js: function(id, data) {
+            $trigger $orig
+        }";
+    }
+    
+    /**
+    * determines wether column currently editable or not
+    * 
+    * @param mixed $model
+    */
+    protected function isEditable($model)
+    {
+         return $model->isAttributeSafe($this->name) && (!array_key_exists('enabled', $this->editable) || $this->editable['enabled'] === true);
+    }
+}

protected/extensions/editable/EditableDetailView.php

+<?php
+/**
+ * EditableDetailView class file.
+ * 
+ * This widget makes editable several attributes of single model, shown as name-value table
+ * 
+ * @author Vitaliy Potapov <noginsk@rambler.ru>
+ * @link https://github.com/vitalets/yii-bootstrap-editable
+ * @copyright Copyright &copy; Vitaliy Potapov 2012
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version 1.0.0
+ */
+ 
+Yii::import('ext.editable.EditableField');
+Yii::import('zii.widgets.CDetailView');
+
+class EditableDetailView extends CDetailView
+{
+    //common url for all editables
+    public $url = '';
+
+    //set bootstrap css
+    public $htmlOptions = array('class'=> 'table table-bordered table-striped table-hover table-condensed');
+
+    public function init()
+    {
+        if (!$this->data instanceof CModel) {
+            throw new CException('Property "data" should be of CModel class.');
+        }
+
+        parent::init();
+    }
+
+    protected function renderItem($options, $templateData)
+    {
+        //if editable set to false --> not editable
+        $isEditable = array_key_exists('editable', $options) && $options['editable'] !== false;
+
+        //if name not defined or it is not safe --> not editable
+        $isEditable = !empty($options['name']) && $this->data->isAttributeSafe($options['name']);
+
+        if ($isEditable) {    
+            //ensure $options['editable'] is array
+            if(!array_key_exists('editable', $options) || !is_array($options['editable'])) $options['editable'] = array();
+
+            //take common url
+            if (!array_key_exists('url', $options['editable'])) {
+                $options['editable']['url'] = $this->url;
+            }
+
+            $editableOptions = CMap::mergeArray($options['editable'], array(
+                'model'     => $this->data,
+                'attribute' => $options['name'],
+                'emptytext' => ($this->nullDisplay === null) ? Yii::t('zii', 'Not set') : strip_tags($this->nullDisplay),
+            ));
+            
+            //if value in detailview options provided, set text directly
+            if(array_key_exists('value', $options) && $options['value'] !== null) {
+                $editableOptions['text'] = $templateData['{value}'];
+                $editableOptions['encode'] = false;
+            }
+
+            $templateData['{value}'] = $this->controller->widget('EditableField', $editableOptions, true);
+        } 
+
+        parent::renderItem($options, $templateData);
+    }
+
+}
+

protected/extensions/editable/EditableField.php

+<?php
+/**
+ * EditableField class file.
+ * 
+ * This widget makes editable single attribute of model
+ * 
+ * @author Vitaliy Potapov <noginsk@rambler.ru>
+ * @link https://github.com/vitalets/yii-bootstrap-editable
+ * @copyright Copyright &copy; Vitaliy Potapov 2012
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version 1.0.0
+ */
+ 
+class EditableField extends CWidget
+{
+    //for all types
+    public $model = null;
+    public $attribute = null;
+    public $type = null;
+    public $url = null;
+    public $title = null;
+    public $emptytext = null;
+    public $text = null; //will be used as content
+    public $value = null;
+    public $placement = null;
+    public $inputclass = null;
+    public $autotext = null;
+
+    //for text & textarea
+    public $placeholder = null;
+    
+    //for select
+    public $source = array();
+    public $prepend = null;
+
+    //for date
+    public $format = null;
+    public $viewformat = null;
+    public $language = null;
+    public $weekStart = null;
+    public $startView = null;
+
+    //methods
+    public $validate = null;
+    public $success = null;
+    public $error = null;
+    
+    //events
+    public $onInit = null;
+    public $onUpdate = null;
+    public $onRender = null;
+    public $onShown = null;
+    public $onHidden = null;
+
+    //js options
+    public $options = array();
+    
+    //html options
+    public $htmlOptions = array();
+
+    //weather to encode text on output
+    public $encode = true;
+
+    //if false text will not be editable, but will be rendered
+    public $enabled = null;
+
+    public function init()
+    {   
+        if (!$this->model) {
+            throw new CException('Parameter "model" should be provided for Editable');
+        }
+        if (!$this->attribute) {
+            throw new CException('Parameter "attribute" should be provided for Editable');
+        }
+        if (!$this->model->hasAttribute($this->attribute)) {
+            throw new CException('Model "'.get_class($this->model).'" does not have attribute "'.$this->attribute.'"');
+        }        
+ 
+        parent::init();
+                
+        if ($this->type === null) {
+            $this->type = 'text';
+            //try detect type from metadata.
+            if (array_key_exists($this->attribute, $this->model->tableSchema->columns)) {
+                $dbType = $this->model->tableSchema->columns[$this->attribute]->dbType;
+                if($dbType == 'date' || $dbType == 'datetime') $this->type = 'date';
+                if(stripos($dbType, 'text') !== false) $this->type = 'textarea';
+            }
+        }
+
+        /*
+        * unfortunatly datepicker's format does not match Yii locale dateFormat
+        * and we cannot take format from application locale
+        * 
+        * see http://www.unicode.org/reports/tr35/#Date_Format_Patterns
+        * 
+        if($this->type == 'date' && $this->format === null) {
+            $this->format = Yii::app()->locale->getDateFormat();
+        }
+        */
+        
+        /* generate text from model attribute (for all types except 'select'. 
+        *  For select/date autotext will be applied)
+        */ 
+        if (!strlen($this->text) && $this->type != 'select' && $this->type != 'date') {
+            $this->text = $this->model->getAttribute($this->attribute);
+        }
+
+        //if enabled not defined directly, set it to true only for safe attributes
+        if($this->enabled === null) {
+            $this->enabled = $this->model->isAttributeSafe($this->attribute);
+        }
+        
+        //if not enabled --> just print text        
+        if (!$this->enabled) {
+            return;
+        }
+
+        //language: use config's value if not defined directly
+        if ($this->language === null && yii::app()->language) {
+            $this->language = yii::app()->language;
+        }
+
+        //normalize url from array if needed
+        $this->url = CHtml::normalizeUrl($this->url);
+
+        //generate title from attribute label
+        if ($this->title === null) {
+            //todo: i18n here. Add messages folder to extension
+            $this->title = (($this->type == 'select' || $this->type == 'date') ? Yii::t('editable', 'Select') : Yii::t('editable', 'Enter')) . ' ' . $this->model->getAttributeLabel($this->attribute);
+        }
+
+        $this->buildHtmlOptions();
+        $this->buildJsOptions();
+        $this->registerAssets();
+    }
+
+    public function buildHtmlOptions()
+    {
+        //html options
+        $htmlOptions = array(
+            'href'      => '#',
+            'rel'       => $this->getSelector(),
+            'data-pk'   => $this->model->primaryKey,
+        );
+
+        //for select we need to define value directly
+        if ($this->type == 'select') {
+            $this->value = $this->model->getAttribute($this->attribute);
+            $this->htmlOptions['data-value'] = $this->value;
+        }
+        
+        //for date we use 'format' to put it into value (if text not defined)
+        if ($this->type == 'date' && !strlen($this->text)) {
+            $this->value = $this->model->getAttribute($this->attribute);
+            
+            //if date comes as object, format it to string
+            if($this->value instanceOf DateTime) {
+                /* 
+                * unfortunatly datepicker's format does not match Yii locale dateFormat,
+                * we need replacements below to convert date correctly
+                */
+                $count = 0;
+                $format = str_replace('MM', 'MMMM', $this->format, $count);
+                if(!$count) $format = str_replace('M', 'MMM', $format, $count);
+                if(!$count) $format = str_replace('m', 'M', $format);
+                
+                $this->value = Yii::app()->dateFormatter->format($format, $this->value->getTimestamp()); 
+            }            
+            
+            $this->htmlOptions['data-value'] = $this->value;
+        }        
+
+        //merging options
+        $this->htmlOptions = CMap::mergeArray($this->htmlOptions, $htmlOptions);
+    }
+
+    public function buildJsOptions()
+    {
+        $options = array(
+            'type'  => $this->type,
+            'url'   => $this->url,
+            'name'  => $this->attribute,
+            'title' => CHtml::encode($this->title),
+        );
+
+        if ($this->emptytext) {
+            $options['emptytext'] = $this->emptytext;
+        }
+        
+        if ($this->placement) {
+            $options['placement'] = $this->placement;
+        }
+        
+        if ($this->inputclass) {
+            $options['inputclass'] = $this->inputclass;
+        }    
+        
+        if ($this->autotext) {
+            $options['autotext'] = $this->autotext;
+        }            
+
+        switch ($this->type) {
+            case 'text':
+            case 'textarea':
+                if ($this->placeholder) {
+                    $options['placeholder'] = $this->placeholder;
+                }
+                break;
+            case 'select':
+                if ($this->source) {
+                    $options['source'] = $this->source;
+                }
+                if ($this->prepend) {
+                    $options['prepend'] = $this->prepend;
+                }
+                break;
+            case 'date':
+                if ($this->format) {
+                    $options['format'] = $this->format;
+                }
+                if ($this->viewformat) {
+                    $options['viewformat'] = $this->viewformat;
+                }                
+                if ($this->language && substr($this->language, 0, 2) != 'en') {
+                    $options['datepicker']['language'] = $this->language;
+                }
+                if ($this->weekStart !== null) {
+                    $options['weekStart'] = $this->weekStart;
+                }
+                if ($this->startView !== null) {
+                    $options['startView'] = $this->startView;
+                }
+                break;
+        }
+
+        //methods
+        foreach(array('validate', 'success', 'error') as $event) {
+            if($this->$event!==null) {
+                $options[$event]=(strpos($this->$event, 'js:') !== 0 ? 'js:' : '') . $this->$event;
+            }
+        }        
+
+        //merging options
+        $this->options = CMap::mergeArray($this->options, $options);
+    }
+
+    public function registerClientScript()
+    {
+        $script = "$('a[rel={$this->htmlOptions['rel']}]')";
+          
+        //attach events
+        foreach(array('init', 'update', 'render', 'shown', 'hidden') as $event) {
+            $property = 'on'.ucfirst($event); 
+            if ($this->$property) {
+                // CJavaScriptExpression appeared only in 1.1.11, will turn to it later
+                //$event = ($this->onInit instanceof CJavaScriptExpression) ? $this->onInit : new CJavaScriptExpression($this->onInit);
+                $eventJs = (strpos($this->$property, 'js:') !== 0 ? 'js:' : '') . $this->$property;
+                $script .= "\n.on('".$event."', ".CJavaScript::encode($eventJs).")";
+            }
+        }
+
+        //apply editable
+        $options = CJavaScript::encode($this->options);        
+        $script .= ".editable($options);";
+        
+        Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id, $script);
+        
+        return $script;
+    }
+
+
+    public function registerAssets()
+    {
+        //if bootstrap extension installed, but no js registered -> register it!
+        if (($bootstrap = yii::app()->getComponent('bootstrap')) && !$bootstrap->enableJS) {
+            $bootstrap->registerCorePlugins(); //enable bootstrap js if needed
+        }
+
+        $assetsUrl = Yii::app()->getAssetManager()->publish(Yii::getPathOfAlias('ext.editable.eassets'), false, 1); //publish excluding datepicker locales
+        Yii::app()->getClientScript()->registerCssFile($assetsUrl . '/css/bootstrap-editable.css');
+        Yii::app()->clientScript->registerScriptFile($assetsUrl . '/js/bootstrap-editable.js', CClientScript::POS_END);
+
+        //include locale for datepicker
+        if ($this->type == 'date' && $this->language && substr($this->language, 0, 2) != 'en') {
+             //todo: check compare dp locale name with yii's
+             $localesUrl = Yii::app()->getAssetManager()->publish(Yii::getPathOfAlias('ext.editable.eassets.js.locales'));
+             Yii::app()->clientScript->registerScriptFile($localesUrl . '/bootstrap-datepicker.'. str_replace('_', '-', $this->language).'.js', CClientScript::POS_END);
+        }
+    }
+
+    public function run()
+    {
+        if($this->enabled) {
+            $this->registerClientScript();
+            $this->renderLink();
+        } else {
+            $this->renderText();
+        }
+    }
+
+    public function renderLink()
+    {
+        echo CHtml::openTag('a', $this->htmlOptions);
+        $this->renderText();
+        echo CHtml::closeTag('a');
+    }
+
+    public function renderText()
+    {   
+        $encodedText = $this->encode ? CHtml::encode($this->text) : $this->text;
+        if($this->type == 'textarea') {
+             $encodedText = preg_replace('/\r?\n/', '<br>', $encodedText);
+        }
+        echo $encodedText;
+    }    
+    
+    public function getSelector()
+    {
+        return get_class($this->model) . '_' . $this->attribute . ($this->model->primaryKey ? '_' . $this->model->primaryKey : '_new');
+    }
+}

protected/extensions/editable/EditableSaver.php

+<?php
+/**
+ * EditableSaver class file.
+ * 
+ * This component is servar-side part for editable widgets. It performs update of one model attribute.
+ * 
+ * @author Vitaliy Potapov <noginsk@rambler.ru>
+ * @link https://github.com/vitalets/yii-bootstrap-editable
+ * @copyright Copyright &copy; Vitaliy Potapov 2012
+ * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
+ * @version 1.0.0
+ */
+ 
+class EditableSaver extends CComponent
+{
+    /**
+     * scenarion used in model for update
+     *
+     * @var mixed
+     */
+    public $scenario = 'editable';
+
+    /**
+     * name of model
+     *
+     * @var mixed
+     */
+    public $modelClass;
+    /**
+     * primaryKey value
+     *
+     * @var mixed
+     */
+    public $primaryKey;
+    /**
+     * name of attribute to be updated
+     *
+     * @var mixed
+     */
+    public $attribute;
+    /**
+     * model instance
+     *
+     * @var CActiveRecord
+     */
+    public $model;
+
+    /**
+     * http status code ruterned for errors
+    */
+    public $errorHttpCode = 400;
+
+    /**
+    * name of changed attributes. Used when saving model
+    * 
+    * @var mixed
+    */
+    protected $changedAttributes = array();
+    
+    /**
+     * Constructor
+     *
+     * @param mixed $modelName
+     * @return EditableBackend
+     */
+    public function __construct($modelClass)
+    {
+        if (empty($modelClass)) {
+            throw new CException(Yii::t('editable', 'You should provide modelClass in constructor of EditableSaver.'));
+        }
+        $this->modelClass = ucfirst($modelClass);
+    }
+
+    /**
+     * main function called to update column in database
+     *
+     */
+    public function update()
+    {
+        //set params from request
+        $this->primaryKey = yii::app()->request->getParam('pk');
+        $this->attribute = yii::app()->request->getParam('name');
+        $value = yii::app()->request->getParam('value');
+
+        //checking params
+        if (empty($this->attribute)) {
+            throw new CException(Yii::t('editable','Property "attribute" should be defined.'));
+        }
+        if (empty($this->primaryKey)) {
+            throw new CException(Yii::t('editable','Property "primaryKey" should be defined.'));
+        }
+
+        //loading model
+        $this->model = CActiveRecord::model($this->modelClass)->findByPk($this->primaryKey);
+        if (!$this->model) {
+            throw new CException(Yii::t('editable', 'Model {class} not found by primary key "{pk}"', array(
+               '{class}'=>get_class($this->model), '{pk}'=>$this->primaryKey)));
+        }
+        $this->model->setScenario($this->scenario);
+        
+        //is attribute exists
+        if (!$this->model->hasAttribute($this->attribute)) {
+            throw new CException(Yii::t('editable', 'Model {class} does not have attribute "{attr}"', array(
+              '{class}'=>get_class($this->model), '{attr}'=>$this->attribute)));            
+        }
+
+        //is attribute safe
+        if (!$this->model->isAttributeSafe($this->attribute)) {
+            throw new CException(Yii::t('editable', 'Model {class} rules do not allow to update attribute "{attr}"', array(
+              '{class}'=>get_class($this->model), '{attr}'=>$this->attribute))); 
+        }
+
+        //setting new value
+        $this->setAttribute($this->attribute, $value);
+
+        //validate
+        $this->model->validate(array($this->attribute));
+        if ($this->model->hasErrors()) {
+            $this->error($this->model->getError($this->attribute));
+        }
+
+        //save
+        if ($this->beforeUpdate()) {
+            //saving (only chnaged attributes)
+            if ($this->model->save(false, $this->changedAttributes)) {
+                $this->afterUpdate();
+            } else {
+                $this->error(Yii::t('editable', 'Error while saving record!')); 
+            }
+        } else {
+            $firstError = reset($this->model->getErrors());
+            $this->error($firstError[0]);
+        }
+    }
+
+    /**
+     * This event is raised before the update is performed.
+     * @param CModelEvent $event the event parameter
+     */
+    public function onBeforeUpdate($event)
+    {
+        $this->raiseEvent('onBeforeUpdate', $event);
+    }
+
+    /**
+     * This event is raised after the update is performed.
+     * @param CEvent $event the event parameter
+     */
+    public function onAfterUpdate($event)
+    {
+        $this->raiseEvent('onAfterUpdate', $event);
+    }
+
+    /**
+     * errors  as CHttpException
+     * @param $msg
+     * @throws CHttpException
+     */
+    protected function error($msg)
+    {
+        throw new CHttpException($this->errorHttpCode, $msg);
+    }
+
+    /**
+     * beforeUpdate
+     *
+     */
+    protected function beforeUpdate()
+    {
+        $this->onBeforeUpdate(new CEvent($this));
+        return !$this->model->hasErrors();
+    }
+
+    /**
+     * afterUpdate
+     *
+     */
+    protected function afterUpdate()
+    {
+        $this->onAfterUpdate(new CEvent($this));
+    }
+    
+    /**
+    * setting new value of attribute.
+    * Attrubute name also stored in array to save only changed attributes
+    * 
+    * @param mixed $name
+    * @param mixed $value
+    */
+    public function setAttribute($name, $value)
+    {
+         $this->model->$name = $value;
+         $this->changedAttributes[] = $name;
+         $this->changedAttributes = array_unique($this->changedAttributes);
+    }
+}

protected/extensions/editable/README.md

+Yii-Bootstrap-Editable
+======================
+
+This extension joins [Yii framework](http://yiiframework.com) with [Bootstrap Editable plugin](http://vitalets.github.com/bootstrap-editable) allowing in-place editing with [Twitter Bootstrap](http://twitter.github.com/bootstrap) Form and Popover.
+Includes three widgets and one component that together give you extremely fast and easy way for creating editable interfaces. 
+
+## Demo & Documentation
+
+http://demopage.ru/yii-bootstrap-editable
+
+## Contributing
+Please make all pull requests against `dev` branch. Your feedback / contribution is very appreciated.
+
+## Questions
+If you have question please feel free to [create an issue](https://github.com/vitalets/yii-bootstrap-editable/issues).

protected/extensions/editable/eassets/CHANGELOG.txt

+Bootstrap-editable change log
+=============================
+
+Version 1.1.4 Sep 25, 2012
+----------------------------
+Enh #40: Added events 'shown' and 'hidden' (@vitalets)
+Bug: Popover position wrong when it is close to screen edge (@vitalets)
+Enh: GetValue api method should return only not-null values (@vitalets)
+Bug: If date not set it's value equals to today (@vitalets)
+Enh #24: Updated to jquery 1.8.2 (@vitalets)
+Enh: Add api method 'submit' to simplify creating new record (@vitalets)
+Enh #39: Add api method 'option' to set one or several options dynamically (@vitalets)
+
+
+Version 1.1.3 Sep 16, 2012
+----------------------------
+Enh #24: Updated to jquery 1.8.1 and bootstrap 2.1.1 (@vitalets)
+Enh: Datepicker updated to last version from eternicode's upstream (@vitalets)
+Enh: Date's default value for 'format' option changed to 'yyyy-mm-dd' (@vitalets)
+Enh #36: DATE: added option 'viewformat' - format for display date, previous 'format' is used when submitting (@vitalets)
+Enh #37: Added event 'render' (@vitalets)
+Enh #34: Clicking outside the popup hides the popup (@vitalets)
+Enh #32: Added methods 'show' and 'hide' to open and close popover manually (@vitalets)
+Bug: Text and textarea are not focused after server validation failed (@vitalets)
+      
+
+Version 1.1.2 Sep 8, 2012
+----------------------------
+Enh #31: Added option 'placeholder' for types 'text' and 'textarea' (@vitalets)
+Enh #30: Added option 'inputclass' to change input's css, default 'span2' (@vitalets)
+Enh #27: Textarea submit by ctrl+enter (@vitalets)
+Bug: Element's content not trimmed when checking for emptytext (@vitalets)
+
+
+Version 1.1.1 Sep 2, 2012
+----------------------------
+Enh: Added event 'init' (@vitalets)
+Enh: Popover shows full error message and re-positioned correctly (@vitalets)
+Bug: Cache for select failed when loading options error occured (@vitalets)
+Enh: Scroll navigation in docs (@vitalets)
+
+
+Version 1.1.0 Aug 26, 2012
+----------------------------
+Enh #23: Added event 'update' (@vitalets)
+Enh: When 'toggle' option defined, no 'editable' css set to element (@vitalets)
+Enh: Smart autotext option for select, loads text only if source defined as object (@vitalets)
+Enh #5: Replace jQuery-ui datepicker with bootstrap-datepicker (@vitalets)
+Enh #22: Select: convert 'source' from native array to object (@vitalets)
+Enh #21: Default value of 'enablefocus' set to false (@vitalets)
+Enh #20: More RESTful: when http status != 200 popover shows responseText (@vitalets)
+Bug #19: Zip opens incorrectly on Mac OS (@vitalets)
+
+
+Version 1.0.5 Aug 8, 2012
+----------------------------
+Bug #18: In submit method value should be set by getInputValue (@vitalets)
+Enh #17: Show response text in error when HTTP != 200 (@vitalets)
+Enh #16: SELECT: set link text according to value (@vitalets)
+Enh #15: Generate release notes (@vitalets)
+Enh #14: Disable focus management (@vitalets)
+Enh #13: Json in html data-* attributes (@vitalets)
+Bug #12: When text contains escaped html it is processed incorrectly (@vitalets)
+Enh #11: Add to dist all required libs (jquery, bootstrap, jquery-ui + theme) (@vitalets)
+Enh #10: SELECT: cache and share source data between selects (@vitalets)
+Enh #9: Pk function in element context (@vitalets)
+Enh #8: Method to customize text render (@vitalets)
+Enh #7: Add empty value to SELECT list (@vitalets)
+
+
+Version 1.0.4 Aug 2, 2012
+----------------------------
+Enh #1: Trigger popover by click on another element (@vitalets)
+Enh #6: Allow inline source for select (@fdev)
+
+
+Version 1.0.3 Jul 20, 2012
+----------------------------
+Enh #3: Tabbular input without mouse (@vitalets)
+Enh #4: Arrange build on Grunt.js (@vitalets)
+                          
+                            
+Version 1.0.2 Jul 19, 2012
+----------------------------
+Enh: Close all popovers when openning new one (@vitalets)      
+                     
+                                                              
+Version 1.0.1 Jul 18, 2012
+----------------------------
+Bug: Ajax not sent properly (@vitalets)
+
+
+Version 1.0.0 Jul 18, 2012
+----------------------------
+Initial release     

protected/extensions/editable/eassets/LICENSE-GPL

+        GNU GENERAL PUBLIC LICENSE
+           Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+          Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+        GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+          NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.

protected/extensions/editable/eassets/LICENSE-MIT

+Copyright (c) 2012 Vitaliy Potapov
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.

protected/extensions/editable/eassets/README.md

+# Bootstrap Editable
+
+In-place editing with Bootstrap Form and Popover
+
+## Demo & Documentation
+
+**http://vitalets.github.com/bootstrap-editable**
+
+## Contributing
+Please make all pull requests against `dev` branch. Thanks!
+
+### Important notes
+Please don't edit files in the `dist` subdirectory as they are generated via grunt. You'll find source code in the `src` subdirectory!
+While grunt can run the included unit tests via PhantomJS, this shouldn't be considered a substitute for the real thing. Please be sure to test the `test/*.html` unit test file(s) in _actual_ browsers.
+
+### Installing grunt
+_This assumes you have [node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed already._
+
+1. Test that grunt is installed globally by running `grunt --version` at the command-line.
+1. If grunt isn't installed globally, run `npm install -g grunt` to install the latest version. _You may need to run `sudo npm install -g grunt`._
+1. From the root directory of this project, run `npm install` to install the project's dependencies.
+
+### Installing PhantomJS
+
+In order for the qunit task to work properly, [PhantomJS](http://www.phantomjs.org/) must be installed and in the system PATH (if you can run "phantomjs" at the command line, this task should work).
+
+Unfortunately, PhantomJS cannot be installed automatically via npm or grunt, so you need to install it yourself. There are a number of ways to install PhantomJS.
+
+* [PhantomJS and Mac OS X](http://ariya.ofilabs.com/2012/02/phantomjs-and-mac-os-x.html)
+* [PhantomJS Installation](http://code.google.com/p/phantomjs/wiki/Installation) (PhantomJS wiki)
+
+Note that the `phantomjs` executable needs to be in the system `PATH` for grunt to see it.
+
+* [How to set the path and environment variables in Windows](http://www.computerhope.com/issues/ch000549.htm)
+* [Where does $PATH get set in OS X 10.6 Snow Leopard?](http://superuser.com/questions/69130/where-does-path-get-set-in-os-x-10-6-snow-leopard)
+* [How do I change the PATH variable in Linux](https://www.google.com/search?q=How+do+I+change+the+PATH+variable+in+Linux)
+
+## License
+Copyright (c) 2012 Vitaliy Potapov  
+Licensed under the MIT, GPL licenses.

protected/extensions/editable/eassets/css/bootstrap-editable.css

+/*! Bootstrap Editable - v1.1.4 
+* In-place editing with Bootstrap Form and Popover
+* https://github.com/vitalets/bootstrap-editable
+* Copyright (c) 2012 Vitaliy Potapov; Licensed MIT, GPL */
+
+a.editable, a.editable:hover {
+  text-decoration: none;
+  border-bottom: dashed 1px #0088cc;
+}
+
+a.editable-open, a.editable-open:hover {
+ 
+}
+
+a.editable-empty {
+  font-style: italic; 
+  color: #DD1144;  
+}
+
+a.editable-changed {
+  font-weight: bold; 
+}
+                 
+.editable-loading {
+  background: url('../img/loading.gif') center center no-repeat;  
+  height: 20px;  
+}
+
+.popover.editable-popover {
+    width: auto; /*for bootstrap 2.1.1*/
+}
+
+.popover.editable-popover .popover-inner {
+   width: auto;
+   max-width: 410px;  /* should be enough for span4 */
+}
+
+.popover.editable-popover div.control-group,
+.popover.editable-popover form,
+.popover.editable-popover span.help-block {
+   margin-bottom: 0;
+}
+
+.popover.editable-popover .form-inline textarea {
+   vertical-align: top;
+}
+
+
+/*!
+ * Datepicker for Bootstrap
+ *
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.datepicker {
+  padding: 4px;
+  margin-top: 1px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  /*.dow {
+		border-top: 1px solid #ddd !important;
+	}*/
+}
+
+.datepicker-inline {
+  width: 220px; 
+/*  height: 220px;  */
+}
+
+.datepicker-float {
+  top: 0;
+  left: 0;    
+}
+
+.datepicker-float:before {
+  content: '';
+  display: inline-block;
+  border-left: 7px solid transparent;
+  border-right: 7px solid transparent;
+  border-bottom: 7px solid #ccc;
+  border-bottom-color: rgba(0, 0, 0, 0.2);
+  position: absolute;
+  top: -7px;
+  left: 6px;
+}
+.datepicker-float:after {
+  content: '';
+  display: inline-block;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #ffffff;
+  position: absolute;
+  top: -6px;
+  left: 7px;
+}
+.datepicker > div {
+  display: none;
+}
+.datepicker.days div.datepicker-days {
+  display: block;
+}
+.datepicker.months div.datepicker-months {
+  display: block;
+}
+.datepicker.years div.datepicker-years {
+  display: block;
+}
+.datepicker table {
+  margin: 0;
+}
+.datepicker td,
+.datepicker th {
+  text-align: center;
+  width: 20px;
+  height: 20px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.datepicker td.day:hover {
+  background: #eeeeee;
+  cursor: pointer;
+}
+.datepicker td.old,
+.datepicker td.new {
+  color: #999999;
+}
+.datepicker td.disabled,
+.datepicker td.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker td.active,
+.datepicker td.active:hover,
+.datepicker td.active.disabled,
+.datepicker td.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:dximagetransform.microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker td.active:hover,
+.datepicker td.active:hover:hover,
+.datepicker td.active.disabled:hover,
+.datepicker td.active.disabled:hover:hover,
+.datepicker td.active:active,
+.datepicker td.active:hover:active,
+.datepicker td.active.disabled:active,
+.datepicker td.active.disabled:hover:active,
+.datepicker td.active.active,
+.datepicker td.active:hover.active,
+.datepicker td.active.disabled.active,
+.datepicker td.active.disabled:hover.active,
+.datepicker td.active.disabled,
+.datepicker td.active:hover.disabled,
+.datepicker td.active.disabled.disabled,
+.datepicker td.active.disabled:hover.disabled,
+.datepicker td.active[disabled],
+.datepicker td.active:hover[disabled],
+.datepicker td.active.disabled[disabled],
+.datepicker td.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker td.active:active,
+.datepicker td.active:hover:active,
+.datepicker td.active.disabled:active,
+.datepicker td.active.disabled:hover:active,
+.datepicker td.active.active,
+.datepicker td.active:hover.active,
+.datepicker td.active.disabled.active,
+.datepicker td.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker td span {
+  display: block;
+  width: 23%;
+  height: 54px;
+  line-height: 54px;
+  float: left;
+  margin: 1%;
+  cursor: pointer;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.datepicker td span:hover {
+  background: #eeeeee;
+}
+.datepicker td span.disabled,
+.datepicker td span.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker td span.active,
+.datepicker td span.active:hover,
+.datepicker td span.active.disabled,
+.datepicker td span.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:dximagetransform.microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker td span.active:hover,
+.datepicker td span.active:hover:hover,
+.datepicker td span.active.disabled:hover,
+.datepicker td span.active.disabled:hover:hover,
+.datepicker td span.active:active,
+.datepicker td span.active:hover:active,
+.datepicker td span.active.disabled:active,
+.datepicker td span.active.disabled:hover:active,
+.datepicker td span.active.active,
+.datepicker td span.active:hover.active,
+.datepicker td span.active.disabled.active,
+.datepicker td span.active.disabled:hover.active,
+.datepicker td span.active.disabled,
+.datepicker td span.active:hover.disabled,
+.datepicker td span.active.disabled.disabled,
+.datepicker td span.active.disabled:hover.disabled,
+.datepicker td span.active[disabled],
+.datepicker td span.active:hover[disabled],
+.datepicker td span.active.disabled[disabled],
+.datepicker td span.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker td span.active:active,
+.datepicker td span.active:hover:active,
+.datepicker td span.active.disabled:active,
+.datepicker td span.active.disabled:hover:active,
+.datepicker td span.active.active,
+.datepicker td span.active:hover.active,
+.datepicker td span.active.disabled.active,
+.datepicker td span.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker td span.old {
+  color: #999999;
+}
+.datepicker th.switch {
+  width: 145px;
+}
+.datepicker thead tr:first-child th {
+  cursor: pointer;
+}
+.datepicker thead tr:first-child th:hover {
+  background: #eeeeee;
+}
+.input-append.date .add-on i,
+.input-prepend.date .add-on i {
+  display: block;
+  cursor: pointer;
+  width: 16px;
+  height: 16px;
+}

protected/extensions/editable/eassets/img/loading.gif

Added
New image

protected/extensions/editable/eassets/js/bootstrap-editable.js

+/*! Bootstrap Editable - v1.1.4 
+* In-place editing with Bootstrap Form and Popover
+* https://github.com/vitalets/bootstrap-editable
+* Copyright (c) 2012 Vitaliy Potapov; Licensed MIT, GPL */
+
+(function ($) {
+
+    //Editable object
+    var Editable = function (element, options) {
+        var type, typeDefaults, doAutotext = false, valueSetByText = false;
+        this.$element = $(element);
+
+        //if exists 'placement' or 'title' options, copy them to data attributes to aplly for popover
+        if (options && options.placement && !this.$element.data('placement')) {
+            this.$element.attr('data-placement', options.placement);
+        }
+        if (options && options.title && !this.$element.data('original-title')) {
+            this.$element.attr('data-original-title', options.title);
+        }
+
+        //detect type
+        type = (this.$element.data().type || (options && options.type) || $.fn.editable.defaults.type);
+        typeDefaults = ($.fn.editable.types[type]) ? $.fn.editable.types[type] : {};
+
+        //apply options
+        this.settings = $.extend({}, $.fn.editable.defaults, $.fn.editable.types.defaults, typeDefaults, options, this.$element.data());
+
+        //apply type's specific init()
+        this.settings.init.call(this, options);
+        
+        //store name
+        this.name = this.settings.name || this.$element.attr('id');
+        if (!this.name) {
+            $.error('You should define name (or id) for Editable element');
+        }
+
+        //if validate is map take only needed function
+        if (typeof this.settings.validate === 'object' && this.name in this.settings.validate) {
+            this.settings.validate = this.settings.validate[this.name];
+        }
+
+        //set value from settings or by element text
+        if (this.settings.value === undefined || this.settings.value === null) {
+            this.settings.setValueByText.call(this);
+            valueSetByText = true;
+        } else {
+            this.value = this.settings.value;
+            valueSetByText = false;
+        }
+
+        //also storing last saved value (initially equals to value)
+        this.lastSavedValue = this.value;
+
+        //set toggle element
+        if (this.settings.toggle) {
+            this.$toggle = $(this.settings.toggle);
+            //insert in DOM if needed
+            if (!this.$toggle.parent().length) {
+                this.$element.after(this.$toggle);
+            }
+            //prevent tabstop on element
+            this.$element.attr('tabindex', -1);
+        } else {
+            this.$toggle = this.$element;
+
+            //add editable class
+            this.$element.addClass('editable');
+        }
+
+        //bind click event on toggle
+        this.$toggle.on('click', $.proxy(this.click, this));
+        
+        //blocking click event when going from inside popover. all other clicks will close it
+        $('body').on('click.editable', '.editable-popover', function (e) { e.stopPropagation(); });
+
+        //autotext
+        if(!valueSetByText && this.value !== null && this.value !== undefined) {
+            switch(this.settings.autotext) {
+              case 'always':
+                doAutotext = true; 
+              break;
+              
+              case 'never':
+                doAutotext = false; 
+              break;
+              
+              case 'auto':
+                if(this.$element.html().length) {
+                   doAutotext = false; 
+                } else {
+                   //for SELECT do not use autotext when source is url and autotext = 'auto' (to prevent extra request)
+                   if (type === 'select') {
+                       this.settings.source = tryParseJson(this.settings.source, true);
+                       if (this.settings.source && typeof this.settings.source === 'object') {
+                           doAutotext = true; 
+                       }
+                   } else {
+                      doAutotext = true; 
+                   }                   
+                }
+              break;
+            }
+        }
+        
+        function finalize() {
+            //show emptytext if visible text is empty
+            this.handleEmpty();
+
+            //trigger 'init' event: DEPRECATED
+            this.$element.trigger('init', this);
+
+            //trigger 'render' event with property isInit = true
+            var event = jQuery.Event("render");
+            event.isInit = true;
+            this.$element.trigger(event, this);
+        }           
+                     
+        if(doAutotext) {
+           $.when(this.settings.setTextByValue.call(this)).then($.proxy(finalize, this));
+        } else {
+           finalize.call(this); 
+        }
+        
+             
+    };
+
+    Editable.prototype = {
+        constructor: Editable,
+
+        click: function (e) {
+            e.stopPropagation();
+            e.preventDefault();
+
+            var popover = this.$element.data('popover');
+            if (popover && popover.tip().is(':visible')) {
+                this.hide();
+            } else {
+                this.show();
+            }
+        },
+
+        show: function () {
+            //hide all other popovers if shown
+            $('.popover').find('form').find('button.editable-cancel').click();
+
+            //for the first time create popover
+            if (!this.$element.data('popover')) {
+                this.$element.popover({
+                    trigger  :'manual',
+                    placement:'top',
+                    content  :this.settings.loading
+                });
+
+                this.$element.data('popover').tip().addClass('editable-popover');
+            }
+
+            //show popover
+            this.$element.popover('show');
+            
+            //movepopover to correct position. Refers to bug in bootstrap 2.1.x with popover positioning
+            this.setPosition();
+            
+            this.$element.addClass('editable-open');
+            this.errorOnRender = false;
+
+            //use deffered approach to load data asynchroniously
+            $.when(this.settings.renderInput.call(this))
+            .then($.proxy(function () {
+                var $tip = this.$element.data('popover').tip();
+
+                //render content & input
+                this.$content = $(this.settings.formTemplate);
+                this.$content.find('div.control-group').prepend(this.$input);
+
+                //invoke form into popover content
+                $tip.find('.popover-content p').append(this.$content);
+                
+                //set position once more. It is required to pre-move popover when it is close to screen edge.
+                this.setPosition();
+
+                //check for error during render input
+                if (this.errorOnRender) {
+                    this.$input.attr('disabled', true);
+                    $tip.find('button.btn-primary').attr('disabled', true);
+                    $tip.find('form').submit(function () {
+                        return false;
+                    });
+                    //show error
+                    this.enableContent(this.errorOnRender);
+                } else {
+                    this.$input.removeAttr('disabled');
+                    $tip.find('button.btn-primary').removeAttr('disabled');
+                    //bind form submit
+                    $tip.find('form').submit($.proxy(this.submit, this));
+                    //show input (and hide loading)
+                    this.enableContent();
+                    //set input value
+                    this.settings.setInputValue.call(this);
+                }
+
+                //bind popover hide on button
+                $tip.find('button.editable-cancel').click($.proxy(this.hide, this));
+
+                //bind popover hide on escape
+                $(document).on('keyup.editable', $.proxy(function (e) {
+                    if (e.which === 27) {
+                        e.stopPropagation();
+                        this.hide();
+                    }
+                }, this));
+               
+                //hide popover on external click
+                $(document).on('click.editable', $.proxy(this.hide, this));
+                
+                //trigger 'shown' event
+                this.$element.trigger('shown', this);
+            }, this));
+        },
+
+        submit: function (e) {
+            e.stopPropagation();
+            e.preventDefault();
+
+            var error,
+                value = this.settings.getInputValue.call(this);
+
+            //validation
+            if (error = this.validate(value)) {
+                this.enableContent(error);
+                return;
+            }
+
+            /*jslint eqeqeq: false*/
+            if (value == this.value) {
+            /*jslint eqeqeq: true*/
+                //if value not changed --> do nothing, simply hide popover
+                this.hide();
+            } else {
+                //saving new value
+                this.save(value);
+            }
+        },
+
+        save: function(value) {
+            $.when(this.send(value))
+            .done($.proxy(function (data) {
+                var error, isAjax = (typeof data !== 'undefined');
+
+                //check and run custom success handler
+                if (isAjax && typeof this.settings.success === 'function' && (error = this.settings.success.apply(this, arguments))) {
+                    //show form with error message
+                    this.enableContent(error);
+                    return;
+                }
+
+                //set new value and text
+                this.value = value;
+                this.settings.setTextByValue.call(this);
+
+                //to show that value modified but not saved
+                if (isAjax) {
+                    this.markAsSaved();
+                } else {
+                    this.markAsUnsaved();
+                }
+
+                this.handleEmpty();
+                this.hide();
+
+                //trigger 'update' event. DEPRECATED! Use 'render' instead.
+                this.$element.trigger('update', this);
+
+                //trigger 'render' event with property isInit = false
+                var event = jQuery.Event("render");
+                event.isInit = false;
+                this.$element.trigger(event, this);
+            }, this))
+            .fail($.proxy(function(xhr) {
+                var msg = (typeof this.settings.error === 'function') ? this.settings.error.apply(this, arguments) : null;
+                this.enableContent(msg || xhr.responseText || xhr.statusText);
+            }, this));
+        },
+
+        send: function(value) {
+            var send, pk, params;
+
+            //getting primary key
+            if (typeof this.settings.pk === 'function') {
+                pk = this.settings.pk.call(this.$element);
+            } else if (typeof this.settings.pk === 'string' && $(this.settings.pk).length === 1 && $(this.settings.pk).parent().length) { //pk is ID of existing element
+                pk = $(this.settings.pk).text();
+            } else {
+                pk = this.settings.pk;
+            }
+
+            send = (this.settings.url !== undefined) && ((this.settings.send === 'always') || (this.settings.send === 'auto' && pk) || (this.settings.send === 'ifpk' /* deprecated */ && pk));
+
+            if (send) { //send to server
+                //hide form, show loading
+                this.enableLoading();
+
+                //try parse json in single quotes
+                this.settings.params = tryParseJson(this.settings.params, true);
+
+                //creating params
+                params = (typeof this.settings.params === 'string') ? {params:this.settings.params} : $.extend({}, this.settings.params);
+                params.name = this.name;
+                params.value = value;
+                if (pk) {
+                    params.pk = pk;
+                }
+
+                //send ajax to server and return deferred object
+                return $.ajax({
+                    url     : (typeof this.settings.url === 'function') ? this.settings.url.call(this) : this.settings.url,
+                    data    : params,
+                    type    : 'post',
+                    dataType: 'json'
+                });
+            }
+        },
+
+        hide: function () {
+            this.$element.popover('hide');
+            this.$element.removeClass('editable-open');
+            $(document).off('keyup.editable');
+            $(document).off('click.editable');
+            
+            //returning focus on toggle element
+            if (this.settings.enablefocus || this.$element.get(0) !== this.$toggle.get(0)) {
+                this.$toggle.focus();
+            }
+            
+            //trigger 'hidden' event
+            this.$element.trigger('hidden', this);
+        },
+
+        /**
+         * show input inside popover
+         */
+        enableContent:function (error) {
+            if (error !== undefined && error.length > 0) {
+                this.$content.find('div.control-group').addClass('error').find('span.help-block').text(error);
+            } else {
+                this.$content.find('div.control-group').removeClass('error').find('span.help-block').text('');
+            }
+            this.$content.show();
+            //hide loading
+            this.$element.data('popover').tip().find('.editable-loading').hide();
+
+            //move popover to final correct position
+            this.setPosition();
+
+            //TODO: find elegant way to exclude hardcode of types here
+            if (this.settings.type === 'text' || this.settings.type === 'textarea') {
+                this.$input.focus();