├── SerialColumn.php ├── composer.json ├── ContextmenuAsset.php ├── KartikSerialColumn.php ├── README.md ├── assets └── js │ └── bootstrap-contextmenu.js └── SerialColumnTrait.php /SerialColumn.php: -------------------------------------------------------------------------------- 1 | 8 | * @date 2015-08-19 9 | */ 10 | class SerialColumn extends \yii\grid\SerialColumn{ 11 | use SerialColumnTrait; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liyunfang/yii2-contextmenu", 3 | "description": "yii2 Extended for bootstrap-contextmenu plugin https://github.com/sydcanem/bootstrap-contextmenu", 4 | "homepage": "https://github.com/liyunfang/yii2-contextmenu", 5 | "type": "yii2-extension", 6 | "keywords": ["yii2","extension","yii2-contextmenu","contextmenu"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "liyunfang", 11 | "email": "381296986@qq.com" 12 | } 13 | ], 14 | "minimum-stability": "dev", 15 | "require": { 16 | "yiisoft/yii2": "*", 17 | "yiisoft/yii2-bootstrap": "*" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "liyunfang\\contextmenu\\": "" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ContextmenuAsset.php: -------------------------------------------------------------------------------- 1 | 9 | * @date 2015-08-19 10 | */ 11 | class ContextmenuAsset extends AssetBundle{ 12 | 13 | /** 14 | * @inheritdoc 15 | */ 16 | public function init() 17 | { 18 | $this->sourcePath = __DIR__ . '/assets'; 19 | $this->js = ['js/bootstrap-contextmenu.js']; 20 | parent::init(); 21 | } 22 | 23 | 24 | public $depends=[ 25 | 'yii\web\YiiAsset', 26 | 'yii\bootstrap\BootstrapAsset', 27 | 'yii\bootstrap\BootstrapPluginAsset', 28 | ]; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /KartikSerialColumn.php: -------------------------------------------------------------------------------- 1 | 9 | * @date 2015-08-27 10 | */ 11 | class KartikSerialColumn extends \kartik\grid\SerialColumn { 12 | use SerialColumnTrait; 13 | 14 | /** 15 | * @inheritdoc 16 | */ 17 | public function renderDataCell($model, $key, $index) 18 | { 19 | if(!$this->_isContextMenu){ 20 | return parent::renderDataCell($model, $key, $index); 21 | } 22 | else{ 23 | $options = $this->fetchContentOptions($model, $key, $index); 24 | $this->parseExcelFormats($options, $model, $key, $index); 25 | $out = $this->renderDataCellContent($model, $key, $index); 26 | return Html::tag('td', $out, $options); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-contextmenu 2 | yii2 Extended for bootstrap-contextmenu 3 | =============================== 4 | yii2 Extended for bootstrap-contextmenu plugin https://github.com/sydcanem/bootstrap-contextmenu 5 | 6 | Gird right - click operation, you can remove the operation column 7 | 8 | ![Effect picture 1](https://github.com/liyunfang/wr/blob/master/images/yii2-contextmenu-1.png "Effect picture 1") 9 | 10 | ![Effect picture 2](https://github.com/liyunfang/wr/blob/master/images/yii2-contextmenu-2.png "Effect picture 2") 11 | 12 | 13 | 14 | 15 | Installation 16 | ------------ 17 | 18 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 19 | 20 | Either run 21 | 22 | ``` 23 | composer require --prefer-dist liyunfang/yii2-contextmenu 24 | ``` 25 | 26 | or add 27 | 28 | ``` 29 | "liyunfang/yii2-contextmenu": "*" 30 | ``` 31 | 32 | to the require section of your `composer.json` file. 33 | 34 | Requirements 35 | ------------ 36 | This extension require twitter-bootstrap 37 | 38 | Usage 39 | ----- 40 | 41 | Once the extension is installed, simply use it in your code by : 42 | 43 | GridView options 44 | ```php 45 | 'columns' => [ 46 | [ 47 | 'class' => \liyunfang\contextmenu\SerialColumn::className(), 48 | 'contextMenu' => true, 49 | //'contextMenuAttribute' => 'id', 50 | 'template' => '{view} {update}
  • {story}', 51 | 'buttons' => [ 52 | 'story' => function ($url, $model) { 53 | $title = Yii::t('app', 'Story'); 54 | $label = ' ' . $title; 55 | $url = \Yii::$app->getUrlManager()->createUrl(['/user/story','id' => $model->id]); 56 | $options = ['tabindex' => '-1','title' => $title, 'data' => ['pjax' => '0' , 'toggle' => 'tooltip']]; 57 | return '
  • ' . Html::a($label, $url, $options) . '
  • ' . PHP_EOL; 58 | } 59 | ], 60 | ], 61 | .... 62 | ], 63 | 'rowOptions' => function($model, $key, $index, $gird){ 64 | $contextMenuId = $gird->columns[0]->contextMenuId; 65 | return ['data'=>[ 'toggle' => 'context','target'=> "#".$contextMenuId ]]; 66 | }, 67 | 68 | ``` 69 | 70 | or use KartikSerialColumn,But this requires the installation of grid Kartik extension. 71 | Please see https://github.com/kartik-v/yii2-grid 72 | 73 | GridView options 74 | ```php 75 | 'columns' => [ 76 | [ 77 | 'class' => \liyunfang\contextmenu\KartikSerialColumn::className(), 78 | 'contextMenu' => true, 79 | //'contextMenuAttribute' => 'id', 80 | //'template' => '{view} {update}', 81 | 'contentOptions'=>['class'=>'kartik-sheet-style'], 82 | 'headerOptions'=>['class'=>'kartik-sheet-style'], 83 | 'urlCreator' => function($action, $model, $key, $index) { 84 | if('update' == $action){ 85 | return \Yii::$app->getUrlManager()->createUrl(['/user/index','id' => $model->id]); 86 | } 87 | if('view' == $action){ 88 | return \Yii::$app->getUrlManager()->createUrl(['/user/view','id' => $model->id]); 89 | } 90 | return '#'; 91 | }, 92 | ], 93 | .... 94 | ], 95 | 'rowOptions' => function($model, $key, $index, $gird){ 96 | $contextMenuId = $gird->columns[0]->contextMenuId; 97 | return ['data'=>[ 'toggle' => 'context','target'=> "#".$contextMenuId ]]; 98 | }, 99 | 100 | ``` 101 | 102 | 103 | 该扩展为gird行右击菜单,可以省去操作列。 104 | 提供了继承yii2 grid的SerialColumn 和 继承 Kartik gird的SerialColumn。 105 | -------------------------------------------------------------------------------- /assets/js/bootstrap-contextmenu.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Context Menu 3 | * Author: @sydcanem 4 | * https://github.com/sydcanem/bootstrap-contextmenu 5 | * 6 | * Inspired by Bootstrap's dropdown plugin. 7 | * Bootstrap (http://getbootstrap.com). 8 | * 9 | * Licensed under MIT 10 | * ========================================================= */ 11 | 12 | ;(function($) { 13 | 14 | 'use strict'; 15 | 16 | /* CONTEXTMENU CLASS DEFINITION 17 | * ============================ */ 18 | var toggle = '[data-toggle="context"]'; 19 | 20 | var ContextMenu = function (element, options) { 21 | this.$element = $(element); 22 | 23 | this.before = options.before || this.before; 24 | this.onItem = options.onItem || this.onItem; 25 | this.scopes = options.scopes || null; 26 | 27 | if (options.target) { 28 | this.$element.data('target', options.target); 29 | } 30 | 31 | this.listen(); 32 | }; 33 | 34 | ContextMenu.prototype = { 35 | 36 | constructor: ContextMenu 37 | ,show: function(e) { 38 | 39 | var $menu 40 | , evt 41 | , tp 42 | , items 43 | , relatedTarget = { relatedTarget: this, target: e.currentTarget }; 44 | 45 | if (this.isDisabled()) return; 46 | 47 | this.closemenu(); 48 | $('[data-toggle="context"]').each(function(){ $(this).contextmenu('closemenu', e); }); 49 | if (this.before.call(this,e,$(e.currentTarget)) === false) return; 50 | 51 | $menu = this.getMenu(); 52 | $menu.trigger(evt = $.Event('show.bs.context', relatedTarget)); 53 | 54 | tp = this.getPosition(e, $menu); 55 | items = 'li:not(.divider)'; 56 | $menu.attr('style', '') 57 | .css(tp) 58 | .addClass('open') 59 | .on('click.context.data-api', items, $.proxy(this.onItem, this, $(e.currentTarget))) 60 | .trigger('shown.bs.context', relatedTarget); 61 | 62 | // Delegating the `closemenu` only on the currently opened menu. 63 | // This prevents other opened menus from closing. 64 | $('html') 65 | .on('click.context.data-api', $menu.selector, $.proxy(this.closemenu, this)); 66 | 67 | return false; 68 | } 69 | 70 | ,closemenu: function(e) { 71 | var $menu 72 | , evt 73 | , items 74 | , relatedTarget; 75 | 76 | $menu = this.getMenu(); 77 | 78 | if(!$menu.hasClass('open')) return; 79 | 80 | relatedTarget = { relatedTarget: this }; 81 | $menu.trigger(evt = $.Event('hide.bs.context', relatedTarget)); 82 | 83 | items = 'li:not(.divider)'; 84 | $menu.removeClass('open') 85 | .off('click.context.data-api', items) 86 | .trigger('hidden.bs.context', relatedTarget); 87 | 88 | $('html') 89 | .off('click.context.data-api', $menu.selector); 90 | // Don't propagate click event so other currently 91 | // opened menus won't close. 92 | //e.stopPropagation(); 93 | 94 | } 95 | 96 | ,keydown: function(e) { 97 | if (e.which == 27) this.closemenu(e); 98 | } 99 | 100 | ,before: function(e) { 101 | return true; 102 | } 103 | 104 | ,onItem: function(e) { 105 | return true; 106 | } 107 | 108 | ,listen: function () { 109 | this.$element.on('contextmenu.context.data-api', this.scopes, $.proxy(this.show, this)); 110 | $('html').on('click.context.data-api', $.proxy(this.closemenu, this)); 111 | $('html').on('keydown.context.data-api', $.proxy(this.keydown, this)); 112 | } 113 | 114 | ,destroy: function() { 115 | this.$element.off('.context.data-api').removeData('context'); 116 | $('html').off('.context.data-api'); 117 | } 118 | 119 | ,isDisabled: function() { 120 | return this.$element.hasClass('disabled') || 121 | this.$element.attr('disabled'); 122 | } 123 | 124 | ,getMenu: function () { 125 | var selector = this.$element.data('target') 126 | , $menu; 127 | 128 | if (!selector) { 129 | selector = this.$element.attr('href'); 130 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, ''); //strip for ie7 131 | } 132 | 133 | $menu = $(selector); 134 | 135 | return $menu && $menu.length ? $menu : this.$element.find(selector); 136 | } 137 | 138 | ,getPosition: function(e, $menu) { 139 | var mouseX = e.clientX 140 | , mouseY = e.clientY 141 | , boundsX = $(window).width() 142 | , boundsY = $(window).height() 143 | , menuWidth = $menu.find('.dropdown-menu').outerWidth() 144 | , menuHeight = $menu.find('.dropdown-menu').outerHeight() 145 | , tp = {"position":"absolute","z-index":9999} 146 | , Y, X, parentOffset; 147 | 148 | if (mouseY + menuHeight > boundsY) { 149 | Y = {"top": mouseY - menuHeight + $(window).scrollTop()}; 150 | } else { 151 | Y = {"top": mouseY + $(window).scrollTop()}; 152 | } 153 | 154 | if ((mouseX + menuWidth > boundsX) && ((mouseX - menuWidth) > 0)) { 155 | X = {"left": mouseX - menuWidth + $(window).scrollLeft()}; 156 | } else { 157 | X = {"left": mouseX + $(window).scrollLeft()}; 158 | } 159 | 160 | // If context-menu's parent is positioned using absolute or relative positioning, 161 | // the calculated mouse position will be incorrect. 162 | // Adjust the position of the menu by its offset parent position. 163 | parentOffset = $menu.offsetParent().offset(); 164 | X.left = X.left - parentOffset.left; 165 | Y.top = Y.top - parentOffset.top; 166 | 167 | return $.extend(tp, Y, X); 168 | } 169 | 170 | }; 171 | 172 | /* CONTEXT MENU PLUGIN DEFINITION 173 | * ========================== */ 174 | 175 | $.fn.contextmenu = function (option,e) { 176 | return this.each(function () { 177 | var $this = $(this) 178 | , data = $this.data('context') 179 | , options = (typeof option == 'object') && option; 180 | 181 | if (!data) $this.data('context', (data = new ContextMenu($this, options))); 182 | if (typeof option == 'string') data[option].call(data, e); 183 | }); 184 | }; 185 | 186 | $.fn.contextmenu.Constructor = ContextMenu; 187 | 188 | /* APPLY TO STANDARD CONTEXT MENU ELEMENTS 189 | * =================================== */ 190 | 191 | $(document) 192 | .on('contextmenu.context.data-api', function() { 193 | $(toggle).each(function () { 194 | var data = $(this).data('context'); 195 | if (!data) return; 196 | data.closemenu(); 197 | }); 198 | }) 199 | .on('contextmenu.context.data-api', toggle, function(e) { 200 | $(this).contextmenu('show', e); 201 | e.preventDefault(); 202 | e.stopPropagation(); 203 | }); 204 | 205 | }(jQuery)); 206 | -------------------------------------------------------------------------------- /SerialColumnTrait.php: -------------------------------------------------------------------------------- 1 | 14 | * @date 2015-08-27 15 | */ 16 | trait SerialColumnTrait { 17 | 18 | /** 19 | * Use contextMenu 20 | */ 21 | public $contextMenu = true; 22 | private $_isContextMenu = false; 23 | private $_contextMenuId = ''; 24 | 25 | 26 | 27 | /** 28 | * The prefix of contextMenu ID 29 | */ 30 | public $contextMenuPrefix = "context-menu"; 31 | /** 32 | * contextMenu ID is generated dynamically by the property 33 | */ 34 | public $contextMenuAttribute = false; 35 | 36 | 37 | 38 | //if extends \kartik\grid\ActionColumn, 39 | public $viewOptions = [ 'data' => ['pjax' => '0' , 'toggle' => 'tooltip']]; 40 | public $updateOptions = [ 'data' => ['pjax' => '0' , 'toggle' => 'tooltip']]; 41 | public $deleteOptions = [ 'data' => ['pjax' => '0' , 'toggle' => 'tooltip' , 'method' => 'post' ]]; 42 | 43 | 44 | public function init() { 45 | $this->_isContextMenu = ($this->grid->bootstrap && $this->contextMenu); 46 | parent::init(); 47 | 48 | if($this->_isContextMenu){ 49 | $this->initDefaultButtons(); 50 | $this->registerAssets(); 51 | } 52 | } 53 | 54 | protected function initDefaultButtons() { 55 | if(!$this->_isContextMenu){ 56 | parent::initDefaultButtons(); 57 | } 58 | else{ 59 | if (!isset($this->buttons['view'])) { 60 | $this->buttons['view'] = function ($url, $model) { 61 | $options = $this->viewOptions; 62 | $title = Yii::t('yii', 'View'); 63 | $icon = ''; 64 | $label = ArrayHelper::remove($options, 'label', $icon . ' ' . $title ); 65 | $options = ArrayHelper::merge(['title' => $title], $options); 66 | $options['tabindex'] = '-1'; 67 | return '
  • ' . Html::a($label, $url, $options) . '
  • ' . PHP_EOL; 68 | }; 69 | } 70 | if (!isset($this->buttons['update'])) { 71 | $this->buttons['update'] = function ($url, $model) { 72 | $options = $this->updateOptions; 73 | $title = Yii::t('yii', 'Update'); 74 | $icon = ''; 75 | $label = ArrayHelper::remove($options, 'label', $icon . ' ' . $title); 76 | $options = ArrayHelper::merge(['title' => $title], $options); 77 | $options['tabindex'] = '-1'; 78 | return '
  • ' . Html::a($label, $url, $options) . '
  • ' . PHP_EOL; 79 | }; 80 | } 81 | if (!isset($this->buttons['delete'])) { 82 | $this->buttons['delete'] = function ($url, $model) { 83 | $options = $this->deleteOptions; 84 | $title = Yii::t('yii', 'Delete'); 85 | $icon = ''; 86 | $label = ArrayHelper::remove($options, 'label', $icon . ' ' . $title); 87 | $options = ArrayHelper::merge([ 'title' => $title, 'data' => ['confirm' => Yii::t('yii', 'Are you sure you want to delete this item?') ] ],$options); 88 | $options['tabindex'] = '-1'; 89 | return '
  • ' . Html::a($label, $url, $options) . '
  • ' . PHP_EOL; 90 | }; 91 | } 92 | } 93 | } 94 | 95 | 96 | protected function renderDataCellContent($model, $key, $index) { 97 | $pageNumContent = parent::renderDataCellContent($model, $key, $index); 98 | if(!$this->_isContextMenu){ 99 | return $pageNumContent; 100 | } 101 | else{ 102 | $content = $this->actionRenderDataCellContent($model, $key, $index); 103 | 104 | $contextMenuId = ''; 105 | if($this->contextMenuPrefix){ 106 | $contextMenuId = $this->contextMenuPrefix.'-'; 107 | } 108 | if(!$this->contextMenuAttribute){ 109 | $contextMenuId .= $this->grid->getId().'-'.$index; 110 | } 111 | else{ 112 | $contextMenuId .= $model->{$this->contextMenuAttribute}; 113 | } 114 | $this->_contextMenuId = $contextMenuId; 115 | $dropdown = Html::tag('ul', $content, ['class' => 'dropdown-menu' , 'role' => 'menu']); 116 | return $pageNumContent.PHP_EOL . Html::tag('div', $dropdown, [ 'id' => $contextMenuId , 'style' => 'display:block;' ]); 117 | } 118 | } 119 | 120 | 121 | /** 122 | * @inheritdoc 123 | */ 124 | public function renderDataCell($model, $key, $index) 125 | { 126 | if(!$this->_isContextMenu){ 127 | return parent::renderDataCell($model, $key, $index); 128 | } 129 | else{ 130 | if ($this->contentOptions instanceof \Closure) { 131 | $options = call_user_func($this->contentOptions, $model, $key, $index, $this); 132 | } else { 133 | $options = $this->contentOptions; 134 | } 135 | 136 | return Html::tag('td', $this->renderDataCellContent($model, $key, $index), $options); 137 | } 138 | } 139 | 140 | 141 | /** 142 | * Registers the needed assets 143 | */ 144 | public function registerAssets() 145 | { 146 | $view = $this->grid->getView(); 147 | ContextmenuAsset::register($view); 148 | } 149 | 150 | 151 | public function getContextMenuId(){ 152 | return $this->_contextMenuId; 153 | } 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | /** 163 | * @var string the ID of the controller that should handle the actions specified here. 164 | * If not set, it will use the currently active controller. This property is mainly used by 165 | * [[urlCreator]] to create URLs for different actions. The value of this property will be prefixed 166 | * to each action name to form the route of the action. 167 | */ 168 | public $controller; 169 | /** 170 | * @var string the template used for composing each cell in the action column. 171 | * Tokens enclosed within curly brackets are treated as controller action IDs (also called *button names* 172 | * in the context of action column). They will be replaced by the corresponding button rendering callbacks 173 | * specified in [[buttons]]. For example, the token `{view}` will be replaced by the result of 174 | * the callback `buttons['view']`. If a callback cannot be found, the token will be replaced with an empty string. 175 | * 176 | * As an example, to only have the view, and update button you can add the ActionColumn to your GridView columns as follows: 177 | * 178 | * ``` 179 | * ['class' => 'yii\grid\ActionColumn', 'template' => '{view} {update}'], 180 | * ``` 181 | * 182 | * @see buttons 183 | */ 184 | public $template = '{view} {update} {delete}'; 185 | /** 186 | * @var array button rendering callbacks. The array keys are the button names (without curly brackets), 187 | * and the values are the corresponding button rendering callbacks. The callbacks should use the following 188 | * signature: 189 | * 190 | * ```php 191 | * function ($url, $model, $key) { 192 | * // return the button HTML code 193 | * } 194 | * ``` 195 | * 196 | * where `$url` is the URL that the column creates for the button, `$model` is the model object 197 | * being rendered for the current row, and `$key` is the key of the model in the data provider array. 198 | * 199 | * You can add further conditions to the button, for example only display it, when the model is 200 | * editable (here assuming you have a status field that indicates that): 201 | * 202 | * ```php 203 | * [ 204 | * 'update' => function ($url, $model, $key) { 205 | * return $model->status === 'editable' ? Html::a('Update', $url) : ''; 206 | * }, 207 | * ], 208 | * ``` 209 | */ 210 | public $buttons = []; 211 | /** 212 | * @var callable a callback that creates a button URL using the specified model information. 213 | * The signature of the callback should be the same as that of [[createUrl()]]. 214 | * If this property is not set, button URLs will be created using [[createUrl()]]. 215 | */ 216 | public $urlCreator; 217 | /** 218 | * @var array html options to be applied to the [[initDefaultButtons()|default buttons]]. 219 | * @since 2.0.4 220 | */ 221 | public $buttonOptions = []; 222 | 223 | 224 | 225 | 226 | /** 227 | * Creates a URL for the given action and model. 228 | * This method is called for each button and each row. 229 | * @param string $action the button name (or action ID) 230 | * @param \yii\db\ActiveRecord $model the data model 231 | * @param mixed $key the key associated with the data model 232 | * @param integer $index the current row index 233 | * @return string the created URL 234 | */ 235 | public function createUrl($action, $model, $key, $index) 236 | { 237 | if ($this->urlCreator instanceof Closure) { 238 | return call_user_func($this->urlCreator, $action, $model, $key, $index); 239 | } else { 240 | $params = is_array($key) ? $key : ['id' => (string) $key]; 241 | $params[0] = $this->controller ? $this->controller . '/' . $action : $action; 242 | 243 | return Url::toRoute($params); 244 | } 245 | } 246 | 247 | /** 248 | * @inheritdoc 249 | */ 250 | protected function actionRenderDataCellContent($model, $key, $index) 251 | { 252 | return preg_replace_callback('/\\{([\w\-\/]+)\\}/', function ($matches) use ($model, $key, $index) { 253 | $name = $matches[1]; 254 | if (isset($this->buttons[$name])) { 255 | $url = $this->createUrl($name, $model, $key, $index); 256 | 257 | return call_user_func($this->buttons[$name], $url, $model, $key); 258 | } else { 259 | return ''; 260 | } 261 | }, $this->template); 262 | } 263 | } 264 | --------------------------------------------------------------------------------