├── 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 | 
9 |
10 | 
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 |
--------------------------------------------------------------------------------