├── MenuAsset.php ├── Module.php ├── README.md ├── assets ├── NestableAsset.php ├── css │ ├── jquery.nestable.css │ └── main.css └── js │ ├── jquery.nestable.js │ ├── main.js │ └── menu_item.js ├── composer.json ├── controllers ├── MenuController.php └── MenuItemController.php ├── messages └── nl │ └── infoweb │ └── menu.php ├── migrations ├── m141008_091012_init.php ├── m141008_110108_add_default_permissions.php ├── m150521_135055_add_anchor_field.php ├── m150723_143855_add_public_column.php ├── m150724_114800_change_entity_field.php ├── m150806_071513_add_position_column.php ├── m150922_114805_change_entity_id_field.php ├── m150929_071512_add_params_column.php └── m150929_071515_add_type_column.php ├── models ├── Menu.php ├── MenuItem.php ├── MenuItemLang.php ├── MenuItemSearch.php ├── MenuSearch.php └── frontend │ └── Menu.php ├── views ├── menu-item │ ├── _data_tab.php │ ├── _default_language_tab.php │ ├── _default_tab.php │ ├── _flash_messages.php │ ├── _form.php │ ├── _search.php │ ├── create.php │ ├── index.php │ └── update.php └── menu │ ├── _flash_messages.php │ ├── _form.php │ ├── _search.php │ ├── create.php │ ├── index.php │ └── update.php └── widgets ├── Nestable.php ├── assets ├── NestableAsset.php ├── css │ └── nestable.css └── js │ └── nestable.js └── views ├── _nestableList.php └── nestable.php /MenuAsset.php: -------------------------------------------------------------------------------- 1 | linkableEntities = ArrayHelper::merge([ 55 | MenuItem::className() => [ 56 | 'label' => 'Menu item', 57 | 'i18nGroup' => 'infoweb/menu', 58 | 'createEntity' => false, 59 | 'createEntityUrl' => '' 60 | ], 61 | Page::className() => [ 62 | 'label' => 'Page', 63 | 'i18nGroup' => 'infoweb/pages', 64 | 'createEntity' => true, 65 | 'createEntityUrl' => '/pages/page/create' 66 | ] 67 | ], $this->linkableEntities); 68 | 69 | // Set eventhandlers 70 | $this->setEventHandlers(); 71 | 72 | // Content duplication is only possible if there is more than 1 app language 73 | if (isset(Yii::$app->params['languages']) && count(Yii::$app->params['languages']) == 1) { 74 | $this->allowContentDuplication = false; 75 | } 76 | } 77 | 78 | /** 79 | * return all needed configuration parameters to javascript. 80 | */ 81 | public function getCkeditorEntitylinkConfiguration() { 82 | return [ 83 | 'linkableEntities' => $this->findLinkableEntities(), 84 | 'url' => [ 85 | 'getEntityUrl' => Url::toRoute('/menu/menu-item/get-entity-url'), 86 | 'getEntities' => Url::toRoute('/menu/menu-item/get-entities') 87 | ], 88 | 'translations' => [ 89 | 'choose' => Yii::t('app', 'Maak een keuze'), 90 | 'no_url' => Yii::t('app', 'Geef de link van de URL') 91 | ] 92 | ]; 93 | } 94 | 95 | /** 96 | * Returns all the entities that can be linked to a menu-item 97 | * 98 | * @return array 99 | */ 100 | public function findLinkableEntities() 101 | { 102 | $linkableEntities = []; 103 | 104 | foreach ($this->linkableEntities as $k => $entity) { 105 | $entityModel = Yii::createObject($k); 106 | 107 | // The entityModel must have the 'getUrl' and 'getAllForDropDownList' methods 108 | if (method_exists($entityModel, 'getUrl') && method_exists($entityModel, 'getAllForDropDownList')) { 109 | $linkableEntities[$k] = [ 110 | 'label' => Yii::t($entity['i18nGroup'], $entity['label']), 111 | 'createEntity' => (isset($entity['createEntity'])) ? (bool) $entity['createEntity'] : false, 112 | 'createEntityUrl' => (isset($entity['createEntityUrl'])) ? (string) $entity['createEntityUrl'] : '' 113 | ]; 114 | } 115 | } 116 | 117 | return $linkableEntities; 118 | } 119 | 120 | public function setEventHandlers() 121 | { 122 | // Set eventhandlers for the 'Menu' model 123 | Event::on(Menu::className(), ActiveRecord::EVENT_AFTER_DELETE, function ($event) { 124 | 125 | // Delete the children 126 | if (!$event->sender->deleteChildren()) { 127 | throw new \yii\base\Exception(Yii::t('app', 'There was an error while deleting this item')); 128 | } 129 | }); 130 | 131 | // Set eventhandlers for the 'MenuItem' model 132 | Event::on(MenuItem::className(), ActiveRecord::EVENT_AFTER_DELETE, function ($event) { 133 | 134 | // Delete the children 135 | if (!$event->sender->deleteChildren()) { 136 | throw new \yii\base\Exception(Yii::t('app', 'There was an error while deleting this item')); 137 | } 138 | }); 139 | } 140 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Menu module for Yii 2 2 | ===================== 3 | 4 | Installation 5 | ------------ 6 | 7 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 8 | 9 | Either run 10 | 11 | ``` 12 | php composer.phar require infoweb-internet-solutions/yii2-cms-menu "*" 13 | ``` 14 | 15 | or add 16 | 17 | ``` 18 | "infoweb-internet-solutions/yii2-cms-menu": "*" 19 | ``` 20 | 21 | to the require section of your `composer.json` file. 22 | 23 | 24 | Usage 25 | ----- 26 | 27 | Once the extension is installed, simply modify your application configuration as follows: 28 | 29 | ```php 30 | 'modules' => [ 31 | ... 32 | 'menu' => [ 33 | 'class' => 'infoweb\menu\Module', 34 | ], 35 | ], 36 | ``` 37 | 38 | Import the translations and use category 'infoweb/menu': 39 | ``` 40 | yii i18n/import @infoweb/menu/messages 41 | ``` 42 | 43 | To use the module, execute yii migration 44 | ``` 45 | yii migrate/up --migrationPath=@vendor/infoweb-internet-solutions/yii2-cms-menu/migrations 46 | ``` 47 | 48 | Configuration 49 | ------------- 50 | All available configuration options are listed below with their default values. 51 | ___ 52 | ##### enablePrivateMenuItems (type: `boolean`, default: `false`) 53 | If this option is set to `true`, the `public` attribute of a menu-item can be managed and the `getTree` function in `models/frontend/Menu` will only return public menu-items if the current application user is a guest. 54 | Keep in mind that you will also have to enable the module in your frontend application to if you set this option to `true`. 55 | ___ 56 | ##### defaultPublicVisibility (type: `boolean`, default: `true`) 57 | This is the value that will be used as the default value of the `public` attribute of a menu-item. 58 | ___ 59 | ##### allowContentDuplication (type: `boolean`, default: `true`) 60 | If this option is set to `true`, the `duplicateable` jquery plugin is activated on all translateable attributes. 61 | ___ 62 | ##### createEntityFromMenuItem (type: `boolean`, default: `true`) 63 | If this option is set to `true`, you can for example create a page in the menu item form. 64 | ___ 65 | ##### linkableEntities (type: `boolean`, default: `[]`) 66 | These are the entities will be used in the `menu` module. 67 | The fully qualified name of the entity class is used as the key in the array. 68 | An entity can only be linked if it implements the `getUrl` and `getAllForDropDownList` methods. 69 | For each configured entity the following fields are required: 70 | - ** label **: The entity label that will be used in the `menu` module 71 | - ** i18nGroup **: The group that will be used for the translation of the label 72 | 73 | Example configuration: 74 | ```php 75 | 'menu' => [ 76 | 'class' => 'infoweb\menu\Module', 77 | 'enablePrivateMenuItems' => true, 78 | 'linkableEntities' => [ 79 | MedicalTraining::className() => [ 80 | 'label' => 'Training', 81 | 'i18nGroup' => 'infoweb/medical-training', 82 | ] 83 | ] 84 | ], 85 | ``` 86 | -------------------------------------------------------------------------------- /assets/NestableAsset.php: -------------------------------------------------------------------------------- 1 | button { 50 | position: relative; 51 | cursor: pointer; 52 | float: left; 53 | width: 25px; 54 | height: 20px; 55 | margin: 5px 0; 56 | padding: 0; 57 | text-indent: 100%; 58 | white-space: nowrap; 59 | overflow: hidden; 60 | border: 0; 61 | background: transparent; 62 | font-size: 12px; 63 | line-height: 1; 64 | text-align: center; 65 | font-weight: bold; 66 | } 67 | .dd-item > button:before { 68 | display: block; 69 | position: absolute; 70 | width: 100%; 71 | text-align: center; 72 | text-indent: 0; 73 | } 74 | .dd-item > button.dd-expand:before { 75 | content: '+'; 76 | } 77 | .dd-item > button.dd-collapse:before { 78 | content: '-'; 79 | } 80 | .dd-expand { 81 | display: none; 82 | } 83 | .dd-collapsed .dd-list, 84 | .dd-collapsed .dd-collapse { 85 | display: none; 86 | } 87 | .dd-collapsed .dd-expand { 88 | display: block; 89 | } 90 | .dd-empty, 91 | .dd-placeholder { 92 | margin: 5px 0; 93 | padding: 0; 94 | min-height: 30px; 95 | background: #f2fbff; 96 | border: 1px dashed #b6bcbf; 97 | box-sizing: border-box; 98 | -moz-box-sizing: border-box; 99 | } 100 | .dd-empty { 101 | border: 1px dashed #bbb; 102 | min-height: 100px; 103 | background-color: #e5e5e5; 104 | background-size: 60px 60px; 105 | background-position: 0 0, 30px 30px; 106 | } 107 | .dd-dragel { 108 | position: absolute; 109 | pointer-events: none; 110 | z-index: 9999; 111 | } 112 | .dd-dragel > .dd-item .dd-handle { 113 | margin-top: 0; 114 | } 115 | .dd-dragel .dd-handle { 116 | box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); 117 | } 118 | 119 | .dd-nochildren .dd-placeholder { 120 | display: none; 121 | } 122 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Nested sortable 3 | */ 4 | 5 | .sortable .fa { 6 | font-size: 18px; 7 | } 8 | 9 | .placeholder { 10 | background-color: #cfcfcf; 11 | } 12 | 13 | .ui-nestedSortable-error { 14 | background: none repeat scroll 0 0 #fbe3e4; 15 | color: #8a1f11; 16 | } 17 | 18 | ol.sortable { 19 | padding: 0; 20 | } 21 | 22 | ol.sortable, 23 | ol.sortable ol { 24 | list-style: none; 25 | margin: 0; 26 | text-align: left; 27 | } 28 | 29 | ol.sortable ol { 30 | padding-left: 30px; 31 | } 32 | 33 | ol.sortable li { 34 | /*border-bottom: solid 1px #ddd;*/ 35 | } 36 | 37 | ol.sortable li div { 38 | line-height: 40px; 39 | margin: 0 0 1px; 40 | /*border-bottom: 1px solid #E9E9E9;*/ 41 | } 42 | 43 | ol.sortable li div .sort { 44 | cursor: move; 45 | float: left; 46 | width: 40px; 47 | text-align: center; 48 | } 49 | 50 | .highlight { 51 | background-color: #FFFF00; 52 | } 53 | 54 | ol.sortable li div:hover { 55 | background-color: #f5f5f5; 56 | } 57 | 58 | ol.sortable .odd { 59 | background-color: #f9f9f9; 60 | } 61 | 62 | .table th.actions { 63 | width:200px; 64 | } 65 | 66 | .action-buttons { 67 | float:right; 68 | width: 180px; 69 | } 70 | 71 | .action-buttons a:hover, 72 | .action-buttons a:focus { 73 | text-decoration: none; 74 | } 75 | 76 | .table-bordered > thead > tr > th, 77 | .table-bordered > thead > tr > td { 78 | border-bottom-width: 1px; 79 | } 80 | 81 | 82 | 83 | /** 84 | * Modal 85 | */ 86 | 87 | .create-page .modal-dialog { 88 | max-width: 1170px; 89 | width: auto; 90 | } 91 | 92 | .create-page .modal-header { 93 | border: none; 94 | } 95 | 96 | -------------------------------------------------------------------------------- /assets/js/jquery.nestable.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Nestable jQuery Plugin - Copyright (c) 2014 Ramon Smit - https://github.com/RamonSmit/Nestable 3 | */ 4 | ; 5 | (function($, window, document, undefined) { 6 | var hasTouch = 'ontouchstart' in window; 7 | 8 | /** 9 | * Detect CSS pointer-events property 10 | * events are normally disabled on the dragging element to avoid conflicts 11 | * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js 12 | */ 13 | var hasPointerEvents = (function() { 14 | var el = document.createElement('div'), 15 | docEl = document.documentElement; 16 | if(!('pointerEvents' in el.style)) { 17 | return false; 18 | } 19 | el.style.pointerEvents = 'auto'; 20 | el.style.pointerEvents = 'x'; 21 | docEl.appendChild(el); 22 | var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto'; 23 | docEl.removeChild(el); 24 | return !!supports; 25 | })(); 26 | 27 | var eStart = hasTouch ? 'touchstart' : 'mousedown', 28 | eMove = hasTouch ? 'touchmove' : 'mousemove', 29 | eEnd = hasTouch ? 'touchend' : 'mouseup', 30 | eCancel = hasTouch ? 'touchcancel' : 'mouseup'; 31 | 32 | var defaults = { 33 | contentCallback: function(item) {return item.content || '' ? item.content : item.id;}, 34 | listNodeName: 'ol', 35 | itemNodeName: 'li', 36 | handleNodeName: 'div', 37 | contentNodeName: 'span', 38 | rootClass: 'dd', 39 | listClass: 'dd-list', 40 | itemClass: 'dd-item', 41 | dragClass: 'dd-dragel', 42 | handleClass: 'dd-handle', 43 | contentClass: 'dd-content', 44 | collapsedClass: 'dd-collapsed', 45 | placeClass: 'dd-placeholder', 46 | noDragClass: 'dd-nodrag', 47 | noChildrenClass: 'dd-nochildren', 48 | emptyClass: 'dd-empty', 49 | expandBtnHTML: '', 50 | collapseBtnHTML: '', 51 | group: 0, 52 | maxDepth: 5, 53 | threshold: 20, 54 | fixedDepth: false, //fixed item's depth 55 | fixed: false, 56 | includeContent: false, 57 | callback: function(l, e) {}, 58 | onDragStart: function(l, e) {}, 59 | listRenderer: function(children, options) { 60 | var html = '<' + options.listNodeName + ' class="' + options.listClass + '">'; 61 | html += children; 62 | html += '' + options.listNodeName + '>'; 63 | 64 | return html; 65 | }, 66 | itemRenderer: function(item_attrs, content, children, options, item) { 67 | var item_attrs_string = $.map(item_attrs, function(value, key) { 68 | return ' ' + key + '="' + value + '"'; 69 | }).join(' '); 70 | 71 | var html = '<' + options.itemNodeName + item_attrs_string + '>'; 72 | html += '<' + options.handleNodeName + ' class="' + options.handleClass + '">'; 73 | html += '<' + options.contentNodeName + ' class="' + options.contentClass + '">'; 74 | html += content; 75 | html += '' + options.contentNodeName + '>'; 76 | html += '' + options.handleNodeName + '>'; 77 | html += children; 78 | html += '' + options.itemNodeName + '>'; 79 | 80 | return html; 81 | } 82 | }; 83 | 84 | function Plugin(element, options) { 85 | this.w = $(document); 86 | this.el = $(element); 87 | if(!options) { 88 | options = defaults; 89 | } 90 | if(options.rootClass !== undefined && options.rootClass !== 'dd') { 91 | options.listClass = options.listClass ? options.listClass : options.rootClass + '-list'; 92 | options.itemClass = options.itemClass ? options.itemClass : options.rootClass + '-item'; 93 | options.dragClass = options.dragClass ? options.dragClass : options.rootClass + '-dragel'; 94 | options.handleClass = options.handleClass ? options.handleClass : options.rootClass + '-handle'; 95 | options.collapsedClass = options.collapsedClass ? options.collapsedClass : options.rootClass + '-collapsed'; 96 | options.placeClass = options.placeClass ? options.placeClass : options.rootClass + '-placeholder'; 97 | options.noDragClass = options.noDragClass ? options.noDragClass : options.rootClass + '-nodrag'; 98 | options.noChildrenClass = options.noChildrenClass ? options.noChildrenClass : options.rootClass + '-nochildren'; 99 | options.emptyClass = options.emptyClass ? options.emptyClass : options.rootClass + '-empty'; 100 | } 101 | 102 | this.options = $.extend({}, defaults, options); 103 | 104 | // build HTML from serialized JSON if passed 105 | if(this.options.json !== undefined) { 106 | this._build(); 107 | } 108 | 109 | this.init(); 110 | } 111 | 112 | Plugin.prototype = { 113 | 114 | init: function() { 115 | var list = this; 116 | 117 | list.reset(); 118 | 119 | list.el.data('nestable-group', this.options.group); 120 | 121 | list.placeEl = $('
'); 122 | 123 | $.each(this.el.find(list.options.itemNodeName), function(k, el) { 124 | var item = $(el), 125 | parent = item.parent(); 126 | list.setParent(item); 127 | if(parent.hasClass(list.options.collapsedClass)) { 128 | list.collapseItem(parent.parent()); 129 | } 130 | }); 131 | 132 | list.el.on('click', 'button', function(e) { 133 | if(list.dragEl || (!hasTouch && e.button !== 0)) { 134 | return; 135 | } 136 | var target = $(e.currentTarget), 137 | action = target.data('action'), 138 | item = target.parent(list.options.itemNodeName); 139 | if(action === 'collapse') { 140 | list.collapseItem(item); 141 | } 142 | if(action === 'expand') { 143 | list.expandItem(item); 144 | } 145 | }); 146 | 147 | var onStartEvent = function(e) { 148 | var handle = $(e.target); 149 | if(!handle.hasClass(list.options.handleClass)) { 150 | if(handle.closest('.' + list.options.noDragClass).length) { 151 | return; 152 | } 153 | handle = handle.closest('.' + list.options.handleClass); 154 | } 155 | if(!handle.length || list.dragEl || (!hasTouch && e.which !== 1) || (hasTouch && e.touches.length !== 1)) { 156 | return; 157 | } 158 | e.preventDefault(); 159 | list.dragStart(hasTouch ? e.touches[0] : e); 160 | }; 161 | 162 | var onMoveEvent = function(e) { 163 | if(list.dragEl) { 164 | e.preventDefault(); 165 | list.dragMove(hasTouch ? e.touches[0] : e); 166 | } 167 | }; 168 | 169 | var onEndEvent = function(e) { 170 | if(list.dragEl) { 171 | e.preventDefault(); 172 | list.dragStop(hasTouch ? e.touches[0] : e); 173 | } 174 | }; 175 | 176 | if(hasTouch) { 177 | list.el[0].addEventListener(eStart, onStartEvent, false); 178 | window.addEventListener(eMove, onMoveEvent, false); 179 | window.addEventListener(eEnd, onEndEvent, false); 180 | window.addEventListener(eCancel, onEndEvent, false); 181 | } 182 | else { 183 | list.el.on(eStart, onStartEvent); 184 | list.w.on(eMove, onMoveEvent); 185 | list.w.on(eEnd, onEndEvent); 186 | } 187 | 188 | var destroyNestable = function() 189 | { 190 | if(hasTouch) { 191 | list.el[0].removeEventListener(eStart, onStartEvent, false); 192 | window.removeEventListener(eMove, onMoveEvent, false); 193 | window.removeEventListener(eEnd, onEndEvent, false); 194 | window.removeEventListener(eCancel, onEndEvent, false); 195 | } 196 | else { 197 | list.el.off(eStart, onStartEvent); 198 | list.w.off(eMove, onMoveEvent); 199 | list.w.off(eEnd, onEndEvent); 200 | } 201 | 202 | list.el.off('click'); 203 | list.el.unbind('destroy-nestable'); 204 | 205 | list.el.data("nestable", null); 206 | }; 207 | 208 | list.el.bind('destroy-nestable', destroyNestable); 209 | 210 | }, 211 | 212 | destroy: function () 213 | { 214 | this.el.trigger('destroy-nestable'); 215 | }, 216 | 217 | _build: function() { 218 | function escapeHtml(text) { 219 | var map = { 220 | '&': '&', 221 | '<': '<', 222 | '>': '>', 223 | '"': '"', 224 | "'": ''' 225 | }; 226 | 227 | return text + "".replace(/[&<>"']/g, function(m) { return map[m]; }); 228 | } 229 | 230 | function filterClasses(classes) { 231 | var new_classes = {}; 232 | 233 | for(var k in classes) { 234 | // Remove duplicates 235 | new_classes[classes[k]] = classes[k]; 236 | } 237 | 238 | return new_classes; 239 | } 240 | 241 | function createClassesString(item, options) { 242 | var classes = item.classes || {}; 243 | 244 | if(typeof classes == 'string') { 245 | classes = [classes]; 246 | } 247 | 248 | var item_classes = filterClasses(classes); 249 | item_classes[options.itemClass] = options.itemClass; 250 | 251 | // create class string 252 | return $.map(item_classes, function(val) { 253 | return val; 254 | }).join(' '); 255 | } 256 | 257 | function createDataAttrs(attr) { 258 | attr = $.extend({}, attr); 259 | 260 | delete attr.children; 261 | delete attr.classes; 262 | delete attr.content; 263 | 264 | var data_attrs = {}; 265 | 266 | $.each(attr, function(key, value) { 267 | if(typeof value == 'object') { 268 | value = JSON.stringify(value); 269 | } 270 | 271 | data_attrs["data-" + key] = escapeHtml(value); 272 | }); 273 | 274 | return data_attrs; 275 | } 276 | 277 | function buildList(items, options) { 278 | if(!items) { 279 | return ''; 280 | } 281 | 282 | var children = ''; 283 | 284 | $.each(items, function(index, sub) { 285 | children += buildItem(sub, options); 286 | }); 287 | 288 | return options.listRenderer(children, options); 289 | } 290 | 291 | function buildItem(item, options) { 292 | var item_attrs = createDataAttrs(item); 293 | item_attrs["class"] = createClassesString(item, options); 294 | 295 | var content = options.contentCallback(item); 296 | var children = buildList(item.children, options); 297 | 298 | return options.itemRenderer(item_attrs, content, children, options, item); 299 | } 300 | 301 | var json = this.options.json; 302 | 303 | if(typeof json == 'string') { 304 | json = JSON.parse(json); 305 | } 306 | 307 | $(this.el).html(buildList(json, this.options)); 308 | }, 309 | 310 | serialize: function() { 311 | var data, list = this, step = function(level) { 312 | var array = [], 313 | items = level.children(list.options.itemNodeName); 314 | items.each(function() { 315 | var li = $(this), 316 | item = $.extend({}, li.data()), 317 | sub = li.children(list.options.listNodeName); 318 | 319 | if(list.options.includeContent) { 320 | var content = li.find('.' + list.options.contentClass).html(); 321 | 322 | if(content) { 323 | item.content = content; 324 | } 325 | } 326 | 327 | if(sub.length) { 328 | item.children = step(sub); 329 | } 330 | array.push(item); 331 | }); 332 | return array; 333 | }; 334 | data = step(list.el.find(list.options.listNodeName).first()); 335 | return data; 336 | }, 337 | 338 | asNestedSet: function() { 339 | var list = this, o = list.options, depth = -1, ret = [], lft = 1; 340 | var items = list.el.find(o.listNodeName).first().children(o.itemNodeName); 341 | 342 | items.each(function () { 343 | lft = traverse(this, depth + 1, lft); 344 | }); 345 | 346 | ret = ret.sort(function(a,b){ return (a.lft - b.lft); }); 347 | return ret; 348 | 349 | function traverse(item, depth, lft) { 350 | var rgt = lft + 1, id, pid; 351 | 352 | if ($(item).children(o.listNodeName).children(o.itemNodeName).length > 0 ) { 353 | depth++; 354 | $(item).children(o.listNodeName).children(o.itemNodeName).each(function () { 355 | rgt = traverse($(this), depth, rgt); 356 | }); 357 | depth--; 358 | } 359 | 360 | id = parseInt($(item).attr('data-id')); 361 | pid = parseInt($(item).parent(o.listNodeName).parent(o.itemNodeName).attr('data-id')) || ''; 362 | 363 | if (id) { 364 | ret.push({"id": id, "parent_id": pid, "depth": depth, "lft": lft, "rgt": rgt}); 365 | } 366 | 367 | lft = rgt + 1; 368 | return lft; 369 | } 370 | }, 371 | 372 | returnOptions: function() { 373 | return this.options; 374 | }, 375 | 376 | serialise: function() { 377 | return this.serialize(); 378 | }, 379 | 380 | reset: function() { 381 | this.mouse = { 382 | offsetX: 0, 383 | offsetY: 0, 384 | startX: 0, 385 | startY: 0, 386 | lastX: 0, 387 | lastY: 0, 388 | nowX: 0, 389 | nowY: 0, 390 | distX: 0, 391 | distY: 0, 392 | dirAx: 0, 393 | dirX: 0, 394 | dirY: 0, 395 | lastDirX: 0, 396 | lastDirY: 0, 397 | distAxX: 0, 398 | distAxY: 0 399 | }; 400 | this.moving = false; 401 | this.dragEl = null; 402 | this.dragRootEl = null; 403 | this.dragDepth = 0; 404 | this.hasNewRoot = false; 405 | this.pointEl = null; 406 | }, 407 | 408 | expandItem: function(li) { 409 | li.removeClass(this.options.collapsedClass); 410 | }, 411 | 412 | collapseItem: function(li) { 413 | var lists = li.children(this.options.listNodeName); 414 | if(lists.length) { 415 | li.addClass(this.options.collapsedClass); 416 | } 417 | }, 418 | 419 | expandAll: function() { 420 | var list = this; 421 | list.el.find(list.options.itemNodeName).each(function() { 422 | list.expandItem($(this)); 423 | }); 424 | }, 425 | 426 | collapseAll: function() { 427 | var list = this; 428 | list.el.find(list.options.itemNodeName).each(function() { 429 | list.collapseItem($(this)); 430 | }); 431 | }, 432 | 433 | setParent: function(li) { 434 | if(li.children(this.options.listNodeName).length) { 435 | // make sure NOT showing two or more sets data-action buttons 436 | li.children('[data-action]').remove(); 437 | li.prepend($(this.options.expandBtnHTML)); 438 | li.prepend($(this.options.collapseBtnHTML)); 439 | } 440 | }, 441 | 442 | unsetParent: function(li) { 443 | li.removeClass(this.options.collapsedClass); 444 | li.children('[data-action]').remove(); 445 | li.children(this.options.listNodeName).remove(); 446 | }, 447 | 448 | dragStart: function(e) { 449 | var mouse = this.mouse, 450 | target = $(e.target), 451 | dragItem = target.closest(this.options.itemNodeName); 452 | 453 | this.options.onDragStart.call(this, this.el, dragItem); 454 | 455 | this.placeEl.css('height', dragItem.height()); 456 | 457 | mouse.offsetX = e.pageX - dragItem.offset().left; 458 | mouse.offsetY = e.pageY - dragItem.offset().top; 459 | mouse.startX = mouse.lastX = e.pageX; 460 | mouse.startY = mouse.lastY = e.pageY; 461 | 462 | this.dragRootEl = this.el; 463 | this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass); 464 | this.dragEl.css('width', dragItem.outerWidth()); 465 | 466 | this.setIndexOfItem(dragItem); 467 | 468 | // fix for zepto.js 469 | //dragItem.after(this.placeEl).detach().appendTo(this.dragEl); 470 | dragItem.after(this.placeEl); 471 | dragItem[0].parentNode.removeChild(dragItem[0]); 472 | dragItem.appendTo(this.dragEl); 473 | 474 | $(document.body).append(this.dragEl); 475 | this.dragEl.css({ 476 | 'left': e.pageX - mouse.offsetX, 477 | 'top': e.pageY - mouse.offsetY 478 | }); 479 | // total depth of dragging item 480 | var i, depth, 481 | items = this.dragEl.find(this.options.itemNodeName); 482 | for(i = 0; i < items.length; i++) { 483 | depth = $(items[i]).parents(this.options.listNodeName).length; 484 | if(depth > this.dragDepth) { 485 | this.dragDepth = depth; 486 | } 487 | } 488 | }, 489 | 490 | setIndexOfItem: function(item, index) { 491 | if((typeof index) === 'undefined') { 492 | index = []; 493 | } 494 | 495 | index.unshift(item.index()); 496 | 497 | if($(item[0].parentNode)[0] !== this.dragRootEl[0]) { 498 | this.setIndexOfItem($(item[0].parentNode), index); 499 | } 500 | else { 501 | this.dragEl.data('indexOfItem', index); 502 | } 503 | }, 504 | 505 | restoreItemAtIndex: function(dragElement) { 506 | var indexArray = this.dragEl.data('indexOfItem'), 507 | currentEl = this.el; 508 | 509 | for(i = 0; i < indexArray.length; i++) { 510 | if((indexArray.length - 1) === parseInt(i)) { 511 | placeElement(currentEl, dragElement); 512 | return 513 | } 514 | currentEl = currentEl[0].children[indexArray[i]]; 515 | } 516 | 517 | function placeElement(currentEl, dragElement) { 518 | if(indexArray[indexArray.length - 1] === 0) { 519 | $(currentEl).prepend(dragElement.clone()); 520 | } 521 | else { 522 | $(currentEl.children[indexArray[indexArray.length - 1] - 1]).after(dragElement.clone()); 523 | } 524 | } 525 | }, 526 | 527 | dragStop: function(e) { 528 | // fix for zepto.js 529 | //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach()); 530 | var el = this.dragEl.children(this.options.itemNodeName).first(); 531 | el[0].parentNode.removeChild(el[0]); 532 | this.placeEl.replaceWith(el); 533 | 534 | if(this.hasNewRoot) { 535 | if(this.options.fixed === true) { 536 | this.restoreItemAtIndex(el); 537 | } 538 | else { 539 | this.el.trigger('lostItem'); 540 | } 541 | this.dragRootEl.trigger('gainedItem'); 542 | } 543 | else { 544 | this.dragRootEl.trigger('change'); 545 | } 546 | 547 | this.dragEl.remove(); 548 | this.options.callback.call(this, this.dragRootEl, el); 549 | 550 | this.reset(); 551 | }, 552 | 553 | dragMove: function(e) { 554 | var list, parent, prev, next, depth, 555 | opt = this.options, 556 | mouse = this.mouse; 557 | 558 | this.dragEl.css({ 559 | 'left': e.pageX - mouse.offsetX, 560 | 'top': e.pageY - mouse.offsetY 561 | }); 562 | 563 | // mouse position last events 564 | mouse.lastX = mouse.nowX; 565 | mouse.lastY = mouse.nowY; 566 | // mouse position this events 567 | mouse.nowX = e.pageX; 568 | mouse.nowY = e.pageY; 569 | // distance mouse moved between events 570 | mouse.distX = mouse.nowX - mouse.lastX; 571 | mouse.distY = mouse.nowY - mouse.lastY; 572 | // direction mouse was moving 573 | mouse.lastDirX = mouse.dirX; 574 | mouse.lastDirY = mouse.dirY; 575 | // direction mouse is now moving (on both axis) 576 | mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1; 577 | mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1; 578 | // axis mouse is now moving on 579 | var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0; 580 | 581 | // do nothing on first move 582 | if(!mouse.moving) { 583 | mouse.dirAx = newAx; 584 | mouse.moving = true; 585 | return; 586 | } 587 | 588 | // calc distance moved on this axis (and direction) 589 | if(mouse.dirAx !== newAx) { 590 | mouse.distAxX = 0; 591 | mouse.distAxY = 0; 592 | } 593 | else { 594 | mouse.distAxX += Math.abs(mouse.distX); 595 | if(mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) { 596 | mouse.distAxX = 0; 597 | } 598 | mouse.distAxY += Math.abs(mouse.distY); 599 | if(mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) { 600 | mouse.distAxY = 0; 601 | } 602 | } 603 | mouse.dirAx = newAx; 604 | 605 | /** 606 | * move horizontal 607 | */ 608 | if(mouse.dirAx && mouse.distAxX >= opt.threshold) { 609 | // reset move distance on x-axis for new phase 610 | mouse.distAxX = 0; 611 | prev = this.placeEl.prev(opt.itemNodeName); 612 | // increase horizontal level if previous sibling exists, is not collapsed, and can have children 613 | if(mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass) && !prev.hasClass(opt.noChildrenClass)) { 614 | // cannot increase level when item above is collapsed 615 | list = prev.find(opt.listNodeName).last(); 616 | // check if depth limit has reached 617 | depth = this.placeEl.parents(opt.listNodeName).length; 618 | if(depth + this.dragDepth <= opt.maxDepth) { 619 | // create new sub-level if one doesn't exist 620 | if(!list.length) { 621 | list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass); 622 | list.append(this.placeEl); 623 | prev.append(list); 624 | this.setParent(prev); 625 | } 626 | else { 627 | // else append to next level up 628 | list = prev.children(opt.listNodeName).last(); 629 | list.append(this.placeEl); 630 | } 631 | } 632 | } 633 | // decrease horizontal level 634 | if(mouse.distX < 0) { 635 | // we can't decrease a level if an item preceeds the current one 636 | next = this.placeEl.next(opt.itemNodeName); 637 | if(!next.length) { 638 | parent = this.placeEl.parent(); 639 | this.placeEl.closest(opt.itemNodeName).after(this.placeEl); 640 | if(!parent.children().length) { 641 | this.unsetParent(parent.parent()); 642 | } 643 | } 644 | } 645 | } 646 | 647 | var isEmpty = false; 648 | 649 | // find list item under cursor 650 | if(!hasPointerEvents) { 651 | this.dragEl[0].style.visibility = 'hidden'; 652 | } 653 | this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop))); 654 | if(!hasPointerEvents) { 655 | this.dragEl[0].style.visibility = 'visible'; 656 | } 657 | if(this.pointEl.hasClass(opt.handleClass)) { 658 | this.pointEl = this.pointEl.closest(opt.itemNodeName); 659 | } 660 | if(this.pointEl.hasClass(opt.emptyClass)) { 661 | isEmpty = true; 662 | } 663 | else if(!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) { 664 | return; 665 | } 666 | 667 | // find parent list of item under cursor 668 | var pointElRoot = this.pointEl.closest('.' + opt.rootClass), 669 | isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id'); 670 | 671 | /** 672 | * move vertical 673 | */ 674 | if(!mouse.dirAx || isNewRoot || isEmpty) { 675 | // check if groups match if dragging over new root 676 | if(isNewRoot && opt.group !== pointElRoot.data('nestable-group')) { 677 | return; 678 | } 679 | 680 | // fixed item's depth, use for some list has specific type, eg:'Volume, Section, Chapter ...' 681 | if(this.options.fixedDepth && this.dragDepth + 1 !== this.pointEl.parents(opt.listNodeName).length) { 682 | return; 683 | } 684 | 685 | // check depth limit 686 | depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length; 687 | if(depth > opt.maxDepth) { 688 | return; 689 | } 690 | var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2); 691 | parent = this.placeEl.parent(); 692 | // if empty create new list to replace empty placeholder 693 | if(isEmpty) { 694 | list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass); 695 | list.append(this.placeEl); 696 | this.pointEl.replaceWith(list); 697 | } 698 | else if(before) { 699 | this.pointEl.before(this.placeEl); 700 | } 701 | else { 702 | this.pointEl.after(this.placeEl); 703 | } 704 | if(!parent.children().length) { 705 | this.unsetParent(parent.parent()); 706 | } 707 | if(!this.dragRootEl.find(opt.itemNodeName).length) { 708 | this.dragRootEl.append(''); 709 | } 710 | // parent root list has changed 711 | this.dragRootEl = pointElRoot; 712 | if(isNewRoot) { 713 | this.hasNewRoot = this.el[0] !== this.dragRootEl[0]; 714 | } 715 | } 716 | } 717 | 718 | }; 719 | 720 | $.fn.nestable = function(params) { 721 | var lists = this, 722 | retval = this; 723 | 724 | if(!('Nestable' in window)) { 725 | window.Nestable = {}; 726 | Nestable.counter = 0; 727 | } 728 | 729 | lists.each(function() { 730 | var plugin = $(this).data("nestable"); 731 | 732 | if(!plugin) { 733 | Nestable.counter++; 734 | $(this).data("nestable", new Plugin(this, params)); 735 | $(this).data("nestable-id", Nestable.counter); 736 | 737 | } 738 | else { 739 | if(typeof params === 'string' && typeof plugin[params] === 'function') { 740 | retval = plugin[params](); 741 | } 742 | } 743 | }); 744 | 745 | return retval || lists; 746 | }; 747 | 748 | })(window.jQuery || window.Zepto, window, document); 749 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Disable all readonly select element options, except for the selected option. 3 | $('select[readonly] option:not(:selected)').attr('disabled', true); 4 | 5 | menu_item.init(); 6 | 7 | // Get anchor 8 | var anchor = menu_item.getParameterByName('anchor'); 9 | // Scroll to anchor 10 | menu_item.scroll_to_element('#' + anchor); 11 | // Hightlight anchor 12 | menu_item.highlight(anchor); 13 | 14 | /** 15 | * ADD ENTITY 16 | */ 17 | var modalElement = $('#create-entity-modal'); 18 | var modalBodyElement = modalElement.find('.modal-body'); 19 | 20 | $(document) 21 | .on('click', '.create-entity-links-link', function(event) { 22 | event.preventDefault(); 23 | 24 | modalBodyElement.html(''); 25 | modalElement.modal('show'); 26 | 27 | $.ajax({ 28 | url: $(this).data('entity-create-url'), 29 | context: document.body 30 | }).done(function(data) { 31 | modalBodyElement.html(data); 32 | modalBodyElement.find('button[name="save-close"], button[name="save-add"]').hide(); 33 | modalElement.modal('show'); 34 | 35 | // Fixes: duplicateable not working but creates a new bug scrollingbar. 36 | // Init the duplicateable jquery plugin: 37 | modalElement.find('[data-duplicateable="true"]').duplicateable(); 38 | }); 39 | }) 40 | .on('submit', '#create-entity-modal .modal-body form', function(e) { 41 | // Make sure we can't submit the form so we need todo ajax validation. 42 | e.preventDefault(); 43 | }) 44 | .on('click', '#create-entity-modal .modal-body button[name="save"]', function(e) { 45 | event.preventDefault(); 46 | 47 | CMS.addLoaderClass(modalBodyElement); 48 | 49 | var form = modalBodyElement.find('form'); 50 | var formaction = form.attr('action'); 51 | var formdata = form.serialize(); 52 | 53 | // Below will trigger submit after validation is done (but we cancel submit above) 54 | form.data('yiiActiveForm').submitting = true; 55 | form.yiiActiveForm('validate'); 56 | 57 | // Hook event so we can check if there are any errors. 58 | form.on('afterValidate', function(ev) { 59 | if(form.find('.has-error').length) { 60 | CMS.removeLoaderClass(modalBodyElement); 61 | CMS.showFirstFormTabWithErrors(); 62 | } 63 | else { 64 | $.ajax({ 65 | method: "POST", 66 | url: formaction+'?saveModel=1', 67 | context: document.body, 68 | data: formdata 69 | }).done(function(response) { 70 | if(response.status == 200) { 71 | // Update dropdown content with pjax 72 | $.pjax.reload({ 73 | container: '#pjax-linkableentities' 74 | }).done(function() { 75 | if(response.status == 200) { 76 | $('#menuitem-entity').trigger('change'); 77 | 78 | var entityID = $('#menuitem-entity').val(); 79 | entityID = entityID.split('\\'); 80 | entityID = entityID[entityID.length-1]; 81 | 82 | // Set the pages dropdown value & update the pages dropdown 83 | $('#'+entityID+'-select2').val(response.id).trigger('change').prop('disabled', false); 84 | } 85 | 86 | CMS.removeLoaderClass(modalBodyElement); 87 | modalElement.modal('hide'); 88 | }); 89 | } 90 | else { 91 | // Remove loader because there is a error. 92 | CMS.removeLoaderClass(modalBodyElement); 93 | alert('Fatal error: Can\'t save the entity.'); 94 | } 95 | }); 96 | } 97 | }); 98 | }) 99 | .on('click', '#create-entity-modal .modal-body a[name="close"]', function(event) { 100 | event.preventDefault(); 101 | modalElement.modal('hide'); 102 | }) 103 | .on("change", "#menuitem-entity", function(event) { 104 | $('.create-entity-links .create-entity-links-link').addClass('hidden'); 105 | var entity = $(this).find('option:selected').val(); 106 | $('.create-entity-links .create-entity-links-link[data-entity="' + entity.split('\\').join('\\\\') + '"]').removeClass('hidden'); 107 | }); 108 | }); -------------------------------------------------------------------------------- /assets/js/menu_item.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | root.menu_item = factory; 3 | })(this, function($) { 4 | 5 | 'use strict'; 6 | 7 | var menu_item = {}; 8 | 9 | menu_item.init = function() { 10 | menu_item.set_eventhandlers(); 11 | }; 12 | 13 | menu_item.set_eventhandlers = function() { 14 | $(document) 15 | .on('change', '#menuitem-entity', menu_item.toggle_attributes) 16 | .on('change', '#menuitem-entity_id', menu_item.togglePageHtmlAnchors); 17 | //.on('click', '#delete-100', menu_item.delete); 18 | }; 19 | /* 20 | menu_item.delete = function(e) { 21 | 22 | e.preventDefault(); 23 | var val = $(this).attr('id').replace('delete-', ''); 24 | 25 | console.log(val); 26 | }; 27 | */ 28 | 29 | menu_item.getParameterByName = function(name) { 30 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 31 | var regex = new RegExp("[\\?&]" + name + "=([^]*)"), 32 | results = regex.exec(location.search); 33 | return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); 34 | }; 35 | 36 | /** 37 | * Scrolls to a element with a given speed 38 | * 39 | * @param string The selector 40 | * @return void 41 | */ 42 | menu_item.scroll_to_element = function(selector, speed) { 43 | // Select the element (limit to the first) 44 | var el = $(selector)[0], 45 | speed = speed || 150; 46 | 47 | // Check for the existance of the element 48 | if($(el).length == 0) 49 | return false; 50 | 51 | // Scroll 52 | $('html,body').animate({scrollTop: $(el).offset().top - 100}, speed); 53 | }; 54 | 55 | /** 56 | * Hightlight an element 57 | * 58 | * @param anchor 59 | * @returns {boolean} 60 | */ 61 | menu_item.highlight = function(anchor) { 62 | 63 | if (anchor.length <= 0) { 64 | return false; 65 | } 66 | 67 | // Save background color 68 | var backgroundColor = $('#' + anchor + ' > div').css('backgroundColor'); 69 | 70 | $('#' + anchor + ' > div').animate({ 71 | // Change background color 72 | backgroundColor: '#fcf8e3' 73 | }, 200, function() { 74 | $(this).delay(3000).animate({ 75 | // Reset background color 76 | backgroundColor: backgroundColor 77 | }, 2000); 78 | }); 79 | }; 80 | 81 | menu_item.toggle_attributes = function(e) { 82 | 83 | // Get the current value 84 | var val = $(this).val(), 85 | parts = val.split('\\'); 86 | val = parts[parts.length - 1]; 87 | 88 | // Hide all attributes 89 | $('.attribute').hide(); 90 | 91 | // Show the attributes that belong to the selected type 92 | if (val) { 93 | $('.'+val+'-attribute').show().find('select').show().prop('disabled', false); 94 | } 95 | 96 | // Only enable the 'menuitem-entity_id' field that is visible 97 | $('.attribute select').prop('disabled', true); 98 | $('.attribute select:visible').prop('disabled', false); 99 | 100 | // Reset the values of the entities 101 | $('#menuitem-entity_id').val(''); 102 | $('#menuitem-url').val(''); 103 | 104 | // Hide the anchors dropdown for a 'page' entity 105 | $('.menu-item-anchor-container').hide(); 106 | }; 107 | 108 | /** 109 | * Toggles the dropdown of a page's html anchors 110 | * 111 | * @param Event 112 | */ 113 | menu_item.togglePageHtmlAnchors = function(e) { 114 | // Get the current value 115 | var val = $('#menuitem-entity').val(), 116 | parts = val.split('\\'); 117 | val = parts[parts.length - 1]; 118 | 119 | // If the entity is a 'page', refresh the anchors dropdown and show it 120 | if (val == 'page' && $(this).val()) { 121 | 122 | var request = $.get('get-page-html-anchors', {page: $(this).val()}); 123 | 124 | request.done(function(response) { 125 | if (response.status == 1) { 126 | // Remove current anchors from the dropdown 127 | $('#menuitem-anchor option').remove(); 128 | 129 | // Add the anchors 130 | $.each(response.anchors, function(i) { 131 | $('#menuitem-anchor').append(''); 132 | }); 133 | 134 | // Show the dropdown if it contains options 135 | if ($('#menuitem-anchor option').length > 1) { 136 | $('.menu-item-anchor-container').show(); 137 | } else { 138 | $('.menu-item-anchor-container').hide(); 139 | } 140 | } 141 | }); 142 | 143 | } else { 144 | $('.menu-item-anchor-container').hide(); 145 | } 146 | }; 147 | 148 | return menu_item; 149 | }(jQuery)); 150 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infoweb-internet-solutions/yii2-cms-menu", 3 | "description": "Menu module for Yii2", 4 | "keywords": ["yii2", "yii2-cms-menu", "menu", "infoweb"], 5 | "type": "yii2-extension", 6 | "license": "MIT", 7 | "support": { 8 | "issues": "https://github.com/infoweb-internet-solutions/yii2-cms-menu/issues?state=open", 9 | "source": "https://github.com/infoweb-internet-solutions/yii2-cms-menu" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Fabio Maggio", 14 | "email": "fabio@infoweb.be", 15 | "homepage": "http://www.infoweb.be/" 16 | }, 17 | { 18 | "name": "Ruben Heymans", 19 | "email": "ruben@infoweb.be", 20 | "homepage": "http://www.infoweb.be/" 21 | } 22 | ], 23 | "require": { 24 | "yiisoft/yii2": "@stable", 25 | "infoweb-internet-solutions/yii2-cms": "@stable" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "infoweb\\menu\\": "" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /controllers/MenuController.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'class' => VerbFilter::className(), 24 | 'actions' => [ 25 | 'delete' => ['post'], 26 | ], 27 | ], 28 | ]; 29 | } 30 | 31 | /** 32 | * Lists all Menu models. 33 | * @return mixed 34 | */ 35 | public function actionIndex() 36 | { 37 | $searchModel = new MenuSearch(); 38 | $dataProvider = $searchModel->search(Yii::$app->request->queryParams); 39 | 40 | return $this->render('index', [ 41 | 'searchModel' => $searchModel, 42 | 'dataProvider' => $dataProvider, 43 | ]); 44 | } 45 | 46 | /** 47 | * Displays a single Menu model. 48 | * @param string $id 49 | * @return mixed 50 | */ 51 | public function actionView($id) 52 | { 53 | return $this->render('view', [ 54 | 'model' => $this->findModel($id), 55 | ]); 56 | } 57 | 58 | /** 59 | * Creates a new Menu model. 60 | * If creation is successful, the browser will be redirected to the 'view' page. 61 | * @return mixed 62 | */ 63 | public function actionCreate() 64 | { 65 | $model = new Menu(); 66 | 67 | $post = Yii::$app->request->post(); 68 | 69 | if ($model->load($post) && $model->save()) { 70 | 71 | // Set flash message 72 | Yii::$app->getSession()->setFlash('menu', Yii::t('app', '"{item}" has been created', ['item' => $model->name])); 73 | 74 | if (isset($post['close'])) { 75 | return $this->redirect(['index']); 76 | } elseif (isset($post['new'])) { 77 | return $this->redirect(['create']); 78 | } else { 79 | return $this->redirect(['update', 'id' => $model->id]); 80 | } 81 | } else { 82 | return $this->render('create', [ 83 | 'model' => $model, 84 | ]); 85 | } 86 | } 87 | 88 | /** 89 | * Updates an existing Menu model. 90 | * If update is successful, the browser will be redirected to the 'view' page. 91 | * @param string $id 92 | * @return mixed 93 | */ 94 | public function actionUpdate($id) 95 | { 96 | $model = $this->findModel($id); 97 | 98 | $post = Yii::$app->request->post(); 99 | 100 | if ($model->load($post) && $model->save()) { 101 | 102 | // Set flash message 103 | Yii::$app->getSession()->setFlash('menu', Yii::t('app', '"{item}" has been updated', ['item' => $model->name])); 104 | 105 | if (isset($post['close'])) { 106 | return $this->redirect(['index']); 107 | } elseif (isset($post['new'])) { 108 | return $this->redirect(['create']); 109 | } else { 110 | return $this->redirect(['update', 'id' => $model->id]); 111 | } 112 | 113 | } else { 114 | return $this->render('update', [ 115 | 'model' => $model, 116 | ]); 117 | } 118 | } 119 | 120 | /** 121 | * Deletes an existing Menu model. 122 | * If deletion is successful, the browser will be redirected to the 'index' page. 123 | * @param string $id 124 | * @return mixed 125 | */ 126 | public function actionDelete($id) 127 | { 128 | try { 129 | // Only the superadmin can delete menu's 130 | if (!Yii::$app->user->can('Superadmin')) 131 | throw new \yii\base\Exception(Yii::t('app', 'You do not have the right permissions to delete this item')); 132 | 133 | $model = $this->findModel($id); 134 | 135 | $transaction = Yii::$app->db->beginTransaction(); 136 | $model->delete(); 137 | $transaction->commit(); 138 | } catch(\yii\base\Exception $e) { 139 | // Set flash message 140 | Yii::$app->getSession()->setFlash('menu-error', $e->getMessage()); 141 | } 142 | 143 | // Set flash message 144 | Yii::$app->getSession()->setFlash('menu', Yii::t('app', '"{item}" has been deleted', ['item' => $model->name])); 145 | 146 | return $this->redirect(['index']); 147 | } 148 | 149 | public function actionMenuItems() 150 | { 151 | return $this->redirect(['menu-item/index']); 152 | } 153 | 154 | /** 155 | * Finds the Menu model based on its primary key value. 156 | * If the model is not found, a 404 HTTP exception will be thrown. 157 | * @param string $id 158 | * @return Menu the loaded model 159 | * @throws NotFoundHttpException if the model cannot be found 160 | */ 161 | protected function findModel($id) 162 | { 163 | if (($model = Menu::findOne($id)) !== null) { 164 | return $model; 165 | } else { 166 | throw new NotFoundHttpException(Yii::t('app', 'The requested item does not exist')); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /controllers/MenuItemController.php: -------------------------------------------------------------------------------- 1 | [ 35 | 'class' => VerbFilter::className(), 36 | 'actions' => [ 37 | 'delete' => ['post'], 38 | 'position' => ['post'], 39 | 'active' => ['post'], 40 | 'update-positions' => ['post'] 41 | ], 42 | ], 43 | ]; 44 | } 45 | 46 | /** 47 | * get all entities 48 | * @return mixed 49 | */ 50 | public function actionGetEntities() { 51 | // Response array 52 | $response = ['status' => 406]; 53 | 54 | $linkableEntities = $this->module->findLinkableEntities(); 55 | 56 | if (Yii::$app->request->post('entity') != null && isset($linkableEntities[Yii::$app->request->post('entity')])) { 57 | $entityModel = Yii::createObject(Yii::$app->request->post('entity')); 58 | 59 | $response['status'] = 200; 60 | $response['data'] = $entityModel->getAllForDropDownList(Yii::$app->request->post('entity_language')); 61 | } 62 | 63 | // Return response in JSON format 64 | Yii::$app->response->format = Response::FORMAT_JSON; 65 | 66 | return $response; 67 | } 68 | 69 | /** 70 | * get entity links 71 | * @return mixed 72 | */ 73 | public function actionGetEntityUrl() { 74 | // Response array 75 | $response = ['status' => 406]; 76 | 77 | $linkableEntities = $this->module->findLinkableEntities(); 78 | 79 | if (Yii::$app->request->post('entity') != null && isset($linkableEntities[Yii::$app->request->post('entity')])) { 80 | $entityModel = Yii::createObject(Yii::$app->request->post('entity')); 81 | $result = $entityModel->findOne(['id' => (int) Yii::$app->request->post('entity_id')]); 82 | 83 | if(method_exists($result, 'getUrl')) { 84 | $response['status'] = 200; 85 | $response['url'] = parse_url($result->getUrl(false, Yii::$app->request->post('entity_language')), PHP_URL_PATH); 86 | 87 | if(isset(Yii::$app->params['developmentFolder'])) { 88 | $response['url'] = str_replace(Yii::$app->params['developmentFolder'], '', $response['url']); 89 | } 90 | } 91 | else { 92 | $response['status'] = 404; 93 | } 94 | } 95 | 96 | // Return response in JSON format 97 | Yii::$app->response->format = Response::FORMAT_JSON; 98 | 99 | return $response; 100 | } 101 | 102 | /** 103 | * Lists all MenuItem models. 104 | * @return mixed 105 | */ 106 | public function actionIndex() 107 | { 108 | // Store the active menu-id in a session var if it is provided through the url 109 | if (Yii::$app->request->get('menu-id') != null) 110 | Yii::$app->session->set('menu-items.menu-id', Yii::$app->request->get('menu-id')); 111 | 112 | $searchModel = new MenuItemSearch(); 113 | $dataProvider = $searchModel->search(Yii::$app->request->queryParams); 114 | $menu = $this->findActiveMenu(); 115 | 116 | return $this->render('index', [ 117 | 'searchModel' => $searchModel, 118 | 'dataProvider' => $dataProvider, 119 | 'menu' => $menu, 120 | 'maxLevel' => json_encode($menu->max_level), 121 | ]); 122 | } 123 | 124 | /** 125 | * Creates a new MenuItem model. 126 | * If creation is successful, the browser will be redirected to the 'view' page. 127 | * @return mixed 128 | */ 129 | public function actionCreate() 130 | { 131 | // Initialize the menu-item with default values 132 | $model = new MenuItem([ 133 | 'menu_id' => $this->findActiveMenu()->id, 134 | 'active' => 1, 135 | 'public' => (int) $this->module->defaultPublicVisibility, 136 | 'type' => MenuItem::TYPE_USER_DEFINED, 137 | ]); 138 | 139 | // The view params 140 | $params = $this->getDefaultViewParams($model); 141 | 142 | if (Yii::$app->request->getIsPost()) { 143 | 144 | $post = Yii::$app->request->post(); 145 | 146 | // Ajax request, validate 147 | if (Yii::$app->request->isAjax) { 148 | return $this->validateModel($model, $post); 149 | // Normal request, save 150 | } else { 151 | return $this->saveModel($model, $post); 152 | } 153 | } 154 | 155 | return $this->render('create', $params); 156 | } 157 | 158 | /** 159 | * Updates an existing MenuItem model. 160 | * If update is successful, the browser will be redirected to the 'view' page. 161 | * @param string $id 162 | * @return mixed 163 | */ 164 | public function actionUpdate($id) 165 | { 166 | // Load the model 167 | $model = $this->findModel($id); 168 | 169 | // The view params 170 | $params = $this->getDefaultViewParams($model); 171 | 172 | if (Yii::$app->request->getIsPost()) { 173 | 174 | $post = Yii::$app->request->post(); 175 | 176 | // Ajax request, validate 177 | if (Yii::$app->request->isAjax) { 178 | 179 | return $this->validateModel($model, $post); 180 | 181 | // Normal request, save models 182 | } else { 183 | return $this->saveModel($model, $post); 184 | } 185 | } 186 | 187 | return $this->render('update', $params); 188 | } 189 | 190 | /** 191 | * Deletes an existing MenuItem model. 192 | * If deletion is successful, the browser will be redirected to the 'index' page. 193 | * @param string $id 194 | * @return mixed 195 | */ 196 | public function actionDelete($id) 197 | { 198 | $model = $this->findModel($id); 199 | $name = $model->name; 200 | 201 | try { 202 | 203 | $transaction = Yii::$app->db->beginTransaction(); 204 | 205 | // Only Superadmin can delete system pages 206 | if ($model->type == MenuItem::TYPE_SYSTEM && !Yii::$app->user->can('Superadmin')) 207 | throw new \yii\base\Exception(Yii::t('app', 'You do not have the right permissions to delete this item')); 208 | 209 | $model->delete(); 210 | $transaction->commit(); 211 | } catch(\yii\base\Exception $e) { 212 | $transaction->rollBack(); 213 | // Set flash message 214 | Yii::$app->getSession()->setFlash('menu-item-error', $e->getMessage()); 215 | 216 | return $this->redirect(['index']); 217 | } 218 | 219 | // Set flash message 220 | Yii::$app->getSession()->setFlash('menu-item', Yii::t('app', '"{item}" has been deleted', ['item' => $name])); 221 | 222 | return $this->redirect(['index']); 223 | } 224 | 225 | /** 226 | * Saves the new positions of the menu items 227 | * @return mixed 228 | */ 229 | public function actionPosition() 230 | { 231 | try { 232 | 233 | $post = Yii::$app->request->post(); 234 | 235 | if(!isset($post['ids'])) 236 | throw new \Exception(Yii::t('infoweb/menu', 'Invalid items')); 237 | 238 | // Delete first item because of bug in nestedsortable 239 | array_shift($post['ids']); 240 | 241 | $positions = array(); 242 | 243 | foreach ($post['ids'] as $k => $v) 244 | { 245 | $item_id = $v['item_id']; 246 | $parent_id = $v['parent_id']; 247 | 248 | // Create menu item 249 | $menu_item = MenuItem::findOne($item_id); 250 | $menu_item->parent_id = (int) $v['parent_id']; 251 | $menu_item->level = $v['depth'] - 1; 252 | 253 | if (!isset($positions["{$menu_item->parent_id}-{$menu_item->level}"])) 254 | $positions["{$menu_item->parent_id}-{$menu_item->level}"] = 0; 255 | 256 | // Set rest of attributes and save 257 | $menu_item->position = $positions["{$menu_item->parent_id}-{$menu_item->level}"] + 1; 258 | $positions["{$menu_item->parent_id}-{$menu_item->level}"]++; 259 | 260 | if (!$menu_item->save()) { 261 | throw new \Exception(Yii::t('app', 'Error while saving')); 262 | } 263 | } 264 | } catch (\Exception $e) { 265 | Yii::error($e->getMessage()); 266 | exit(); 267 | } 268 | 269 | $data['status'] = 1; 270 | 271 | Yii::$app->response->format = 'json'; 272 | return $data; 273 | } 274 | 275 | /** 276 | * Set active state 277 | * @param string $id 278 | * @return mixed 279 | */ 280 | public function actionActive() 281 | { 282 | Yii::$app->response->format = Response::FORMAT_JSON; 283 | $model = $this->findModel(Yii::$app->request->post('id')); 284 | $model->active = ($model->active == 1) ? 0 : 1; 285 | 286 | $data['status'] = $model->save(); 287 | $data['active'] = $model->active; 288 | 289 | // Update the status of the children 290 | foreach ($model->children as $child) { 291 | $child->active = $model->active; 292 | $child->save(); 293 | } 294 | 295 | return $data; 296 | } 297 | 298 | /** 299 | * Returns a page's html anchors 300 | * 301 | * @return json 302 | */ 303 | public function actionGetPageHtmlAnchors() 304 | { 305 | Yii::$app->response->format = Response::FORMAT_JSON; 306 | $response = [ 307 | 'status' => 0, 308 | 'msg' => '', 309 | 'anchors' => ['' => Yii::t('infoweb/menu', 'Choose an anchor')] 310 | ]; 311 | 312 | $page = Page::findOne(Yii::$app->request->get('page')); 313 | 314 | if ($page) { 315 | $response['anchors'] = array_merge($response['anchors'], $page->htmlAnchors); 316 | } 317 | 318 | $response['status'] = 1; 319 | 320 | return $response; 321 | } 322 | 323 | /** 324 | * Set public state 325 | * @param string $id 326 | * @return mixed 327 | */ 328 | public function actionPublic() 329 | { 330 | $model = $this->findModel(Yii::$app->request->post('id')); 331 | $model->public = ($model->public == 1) ? 0 : 1; 332 | 333 | // Ajax request 334 | if (Yii::$app->request->isAjax) { 335 | Yii::$app->response->format = Response::FORMAT_JSON; 336 | $response = ['status' => 0]; 337 | 338 | if ($model->save()) { 339 | $response['status'] = 1; 340 | $response['public'] = $model->public; 341 | } 342 | 343 | return $response; 344 | // Normal request 345 | } else { 346 | return $model->save(); 347 | } 348 | } 349 | 350 | /** 351 | * Updates the positions of the provided menu items 352 | * @return mixed 353 | */ 354 | public function actionUpdatePositions() 355 | { 356 | Yii::$app->response->format = Response::FORMAT_JSON; 357 | $response = [ 358 | 'status' => 0, 359 | 'msg' => '' 360 | ]; 361 | 362 | try { 363 | $items = Yii::$app->request->post('items'); 364 | 365 | // Wrap the everything in a database transaction 366 | $transaction = Yii::$app->db->beginTransaction(); 367 | 368 | if (!$this->updatePositions($items)) { 369 | throw new \Exception(Yii::t('app', 'Error while saving')); 370 | } 371 | 372 | $transaction->commit(); 373 | $response['status'] = 1; 374 | } catch (\Exception $e) { 375 | $response['msg'] = $e->getMessage(); 376 | } 377 | 378 | return $response; 379 | } 380 | 381 | /** 382 | * Finds the MenuItem model based on its primary key value. 383 | * If the model is not found, a 404 HTTP exception will be thrown. 384 | * @param string $id 385 | * @return MenuItem the loaded model 386 | * @throws NotFoundHttpException if the model cannot be found 387 | */ 388 | protected function findModel($id) 389 | { 390 | if (($model = MenuItem::findOne($id)) !== null) { 391 | return $model; 392 | } else { 393 | throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist')); 394 | } 395 | } 396 | 397 | /** 398 | * Returns an array of the default params that are passed to a view 399 | * 400 | * @param Page $model The model that has to be passed to the view 401 | * @return array 402 | */ 403 | protected function getDefaultViewParams($model = null) 404 | { 405 | return [ 406 | 'model' => $model, 407 | 'menu' => $this->findActiveMenu(), 408 | 'linkableEntities' => $this->findLinkableEntities(), 409 | 'entityTypes' => $this->entityTypes(), 410 | 'allowContentDuplication' => $this->module->allowContentDuplication 411 | ]; 412 | } 413 | 414 | protected function findActiveMenu() 415 | { 416 | // If no valid active menu-id is set, search the first menu and use it's id 417 | if (in_array(Yii::$app->session->get('menu-items.menu-id'), [0, null])) { 418 | $menu = Menu::find()->one(); 419 | Yii::$app->session->set('menu-items.menu-id', $menu->id); 420 | } else { 421 | $menu = Menu::findone(Yii::$app->session->get('menu-items.menu-id')); 422 | } 423 | 424 | return $menu; 425 | } 426 | 427 | /** 428 | * Returns a combination of default entityTypes and the one's that are set 429 | * in the controller. 430 | * 431 | * @return array 432 | */ 433 | protected function entityTypes() 434 | { 435 | return ArrayHelper::merge($this->entityTypes, [ 436 | 'url' => Yii::t('app', 'Url'), 437 | 'none' => Yii::t('infoweb/menu', 'Nothing'), 438 | ]); 439 | } 440 | 441 | /** 442 | * Returns all the entities that can be linked to a menu-item 443 | * 444 | * @return array 445 | */ 446 | protected function findLinkableEntities() 447 | { 448 | $linkableEntities = $this->module->findLinkableEntities(); 449 | 450 | foreach($linkableEntities as $k => $entity) { 451 | $entityModel = Yii::createObject($k); 452 | $linkableEntities[$k]['data'] = $entityModel->getAllForDropDownList(); 453 | 454 | // Add it also to the entityTypes variable of the controller 455 | $this->entityTypes[$k] = $entity['label']; 456 | } 457 | 458 | return $linkableEntities; 459 | } 460 | 461 | /** 462 | * Performs validation on the provided model and $_POST data 463 | * 464 | * @param \infoweb\pages\models\Page $model The page model 465 | * @param array $post The $_POST data 466 | * @return array 467 | */ 468 | protected function validateModel($model, $post) 469 | { 470 | $languages = Yii::$app->params['languages']; 471 | 472 | // Populate the model with the POST data 473 | $model->load($post); 474 | 475 | // Parent is root 476 | if (empty($post[StringHelper::basename(MenuItem::className())]['parent_id'])) { 477 | $model->parent_id = 0; 478 | $model->level = 0; 479 | } else { 480 | $parent = MenuItem::findOne($post[StringHelper::basename(MenuItem::className())]['parent_id']); 481 | $model->parent_id = $parent->id; 482 | $model->level = $parent->level + 1; 483 | } 484 | 485 | // Create an array of translation models and populate them 486 | $translationModels = []; 487 | // Insert 488 | if ($model->isNewRecord) { 489 | foreach ($languages as $languageId => $languageName) { 490 | $translationModels[$languageId] = new MenuItemLang(['language' => $languageId]); 491 | } 492 | // Update 493 | } else { 494 | $translationModels = ArrayHelper::index($model->getTranslations()->all(), 'language'); 495 | } 496 | Model::loadMultiple($translationModels, $post); 497 | 498 | // Validate the model and translation 499 | $response = array_merge( 500 | ActiveForm::validate($model), 501 | ActiveForm::validateMultiple($translationModels) 502 | ); 503 | 504 | // Return validation in JSON format 505 | Yii::$app->response->format = Response::FORMAT_JSON; 506 | return $response; 507 | } 508 | 509 | protected function saveModel($model, $post) 510 | { 511 | // Wrap everything in a database transaction 512 | $transaction = Yii::$app->db->beginTransaction(); 513 | 514 | // Get the params 515 | $params = $this->getDefaultViewParams($model); 516 | $currentParentId = $model->parent_id; 517 | 518 | // Populate the model with the POST data 519 | $model->load($post); 520 | 521 | // Set level and calculate next position for a new record or when 522 | // the parent has changed 523 | if ($model->isNewRecord || (!$model->isNewRecord && $currentParentId != $model->parent_id)) { 524 | $model->level = ($model->parent_id) ? $model->parent->level + 1 : 0; 525 | $model->position = $model->nextPosition(); 526 | } 527 | 528 | // Validate the main model 529 | if (!$model->validate()) { 530 | return $this->render($this->action->id, $params); 531 | } 532 | 533 | // Add the translations 534 | foreach (Yii::$app->request->post(StringHelper::basename(MenuItemLang::className()), []) as $language => $data) { 535 | foreach ($data as $attribute => $translation) { 536 | $model->translate($language)->$attribute = $translation; 537 | } 538 | } 539 | 540 | // Save the main model 541 | if (!$model->save()) { 542 | return $this->render($this->action->id, $params); 543 | } 544 | 545 | $transaction->commit(); 546 | 547 | // Set flash message 548 | if ($this->action->id == 'create') { 549 | Yii::$app->getSession()->setFlash('menu-item', Yii::t('app', '"{item}" has been created', ['item' => $model->name])); 550 | } else { 551 | Yii::$app->getSession()->setFlash('menu-item', Yii::t('app', '"{item}" has been updated', ['item' => $model->name])); 552 | } 553 | 554 | // Take appropriate action based on the pushed button 555 | if (isset($post['save-close'])) { 556 | return $this->redirect(['index']); 557 | } elseif (isset($post['save-add'])) { 558 | return $this->redirect(['create']); 559 | } else { 560 | return $this->redirect(['update', 'id' => $model->id]); 561 | } 562 | } 563 | 564 | /** 565 | * Updates the positions of the provided menu items 566 | * 567 | * @param array $items The menu items 568 | * @param infoweb\menu\models\MenuItem 569 | * @return boolean 570 | */ 571 | protected function updatePositions($items = [], $parent = null) 572 | { 573 | // Determine the parentId and level 574 | $parentId = ($parent !== null) ? $parent->id : 0; 575 | $level = ($parent !== null) ? $parent->level + 1 : 0; 576 | 577 | foreach ($items as $k => $item) { 578 | // Update the menu item 579 | $menuItem = MenuItem::findOne($item['id']); 580 | $menuItem->parent_id = $parentId; 581 | $menuItem->level = $level; 582 | $menuItem->position = $k + 1; 583 | 584 | if (!$menuItem->save()) { 585 | throw new \Exception("Error while saving menuItem #{$menuItem->id}"); 586 | } 587 | 588 | // Update the position of the item's children 589 | if (isset($item['children'])) { 590 | $this->updatePositions($item['children'], $menuItem); 591 | } 592 | } 593 | 594 | return true; 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /messages/nl/infoweb/menu.php: -------------------------------------------------------------------------------- 1 | '', 5 | 'Choose a menu item' => '', 6 | 'Choose an anchor' => '', 7 | 'Level' => '', 8 | 'Menu' => '', 9 | 'Menu ID' => '', 10 | 'Menu item' => '', 11 | 'Menu items' => '', 12 | 'Menu\'s' => '', 13 | 'Menus' => '', 14 | 'Nothing' => 'Niets', 15 | 'Parent ID' => '', 16 | 'Public' => '', 17 | 'anchor' => '', 18 | ]; 19 | -------------------------------------------------------------------------------- /migrations/m141008_091012_init.php: -------------------------------------------------------------------------------- 1 | db->driverName === 'mysql') { 13 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; 14 | } 15 | 16 | // Drop the default user table 17 | if ($this->db->schema->getTableSchema('menu', true) !== null) { 18 | $this->dropTable('{{%menu}}'); 19 | } 20 | 21 | // Create 'menu' table 22 | $this->createTable('{{%menu}}', [ 23 | 'id' => Schema::TYPE_PK, 24 | 'name' => Schema::TYPE_STRING . '(255) NOT NULL', 25 | 'max_level' => 'TINYINT(3) UNSIGNED NOT NULL DEFAULT \'2\'', 26 | 'created_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 27 | 'updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 28 | ], $tableOptions); 29 | 30 | // Create 'menu_item' table 31 | $this->createTable('{{%menu_item}}', [ 32 | 'id' => Schema::TYPE_PK, 33 | 'menu_id' => Schema::TYPE_INTEGER . ' NOT NULL', 34 | 'parent_id' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 35 | 'entity' => "ENUM('page','menu-item', 'url') NOT NULL DEFAULT 'page'", 36 | 'entity_id' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 37 | 'level' => 'TINYINT(3) UNSIGNED NOT NULL DEFAULT \'0\'', 38 | 'url' => Schema::TYPE_STRING . '(255) NOT NULL', 39 | 'position' => 'TINYINT(3) UNSIGNED NOT NULL DEFAULT \'0\'', 40 | 'active' => 'TINYINT(3) UNSIGNED NOT NULL DEFAULT \'1\'', 41 | 'created_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 42 | 'updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 43 | ], $tableOptions); 44 | 45 | // Create indexes on the 'menu_item' table 46 | $this->createIndex('menu_id', '{{%menu_item}}', 'menu_id'); 47 | $this->createIndex('parent_id', '{{%menu_item}}', 'parent_id'); 48 | $this->createIndex('entity', '{{%menu_item}}', 'entity'); 49 | $this->createIndex('entity_id', '{{%menu_item}}', 'entity_id'); 50 | $this->addForeignKey('FK_MENU_ITEM_MENU_ID', '{{%menu_item}}', 'menu_id', '{{%menu}}', 'id', 'CASCADE', 'RESTRICT'); 51 | 52 | // Create 'menu_item_lang' table 53 | $this->createTable('{{%menu_item_lang}}', [ 54 | 'menu_item_id' => Schema::TYPE_INTEGER . ' NOT NULL', 55 | 'language' => Schema::TYPE_STRING . '(10) NOT NULL', 56 | 'name' => Schema::TYPE_STRING . '(255) NOT NULL', 57 | 'created_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL', 58 | 'updated_at' => Schema::TYPE_INTEGER . ' UNSIGNED NOT NULL' 59 | ], $tableOptions); 60 | 61 | // Create indexes on the 'menu_item_lang' table 62 | $this->addPrimaryKey('menu_item_menu_id_language', '{{%menu_item_lang}}', ['menu_item_id', 'language']); 63 | $this->createIndex('language', '{{%menu_item_lang}}', 'language'); 64 | $this->addForeignKey('FK_MENU_ITEM_LANG_MENU_ITEM_ID', '{{%menu_item_lang}}', 'menu_item_id', '{{%menu_item}}', 'id', 'CASCADE', 'RESTRICT'); 65 | } 66 | 67 | public function down() 68 | { 69 | $this->dropTable('menu_item_lang'); 70 | $this->dropTable('menu_item'); 71 | $this->dropTable('menu'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /migrations/m141008_110108_add_default_permissions.php: -------------------------------------------------------------------------------- 1 | insert('{{%auth_item}}', [ 12 | 'name' => 'showMenuModule', 13 | 'type' => 2, 14 | 'description' => 'Show menu module in main-menu', 15 | 'created_at' => time(), 16 | 'updated_at' => time() 17 | ]); 18 | 19 | $this->insert('{{%auth_item}}', [ 20 | 'name' => 'createMenu', 21 | 'type' => 2, 22 | 'description' => 'Create a new menu in the menu module', 23 | 'created_at' => time(), 24 | 'updated_at' => time() 25 | ]); 26 | 27 | // Create the auth item relation 28 | $this->insert('{{%auth_item_child}}', [ 29 | 'parent' => 'Superadmin', 30 | 'child' => 'showMenuModule' 31 | ]); 32 | 33 | $this->insert('{{%auth_item_child}}', [ 34 | 'parent' => 'Superadmin', 35 | 'child' => 'createMenu' 36 | ]); 37 | } 38 | 39 | public function down() 40 | { 41 | // Delete the auth item relation 42 | $this->delete('{{%auth_item_child}}', [ 43 | 'parent' => 'Superadmin', 44 | 'child' => 'showMenuModule' 45 | ]); 46 | 47 | $this->delete('{{%auth_item_child}}', [ 48 | 'parent' => 'Superadmin', 49 | 'child' => 'createMenu' 50 | ]); 51 | 52 | // Delete the auth items 53 | $this->delete('{{%auth_item}}', [ 54 | 'name' => 'showMenuModule', 55 | 'type' => 2, 56 | ]); 57 | 58 | $this->delete('{{%auth_item}}', [ 59 | 'name' => 'createMenu', 60 | 'type' => 2, 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /migrations/m150521_135055_add_anchor_field.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%menu_item}}', 'anchor', Schema::TYPE_STRING.'(255) NOT NULL'); 11 | } 12 | 13 | public function down() 14 | { 15 | $this->dropColumn('{{%menu_item}}', 'anchor'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /migrations/m150723_143855_add_public_column.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%menu_item}}', 'public', Schema::TYPE_BOOLEAN.' UNSIGNED NOT NULL DEFAULT 1'); 11 | } 12 | 13 | public function down() 14 | { 15 | $this->dropColumn('{{%menu_item}}', 'public'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /migrations/m150724_114800_change_entity_field.php: -------------------------------------------------------------------------------- 1 | alterColumn('{{%menu_item}}', 'entity', Schema::TYPE_STRING.'(255) NOT NULL DEFAULT \'page\''); 11 | } 12 | 13 | public function down() 14 | { 15 | echo "m150724_114800_change_entity_field cannot be reverted.\n"; 16 | 17 | return false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/m150806_071513_add_position_column.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%menu}}', 'position', Schema::TYPE_INTEGER.' UNSIGNED NOT NULL'); 11 | } 12 | 13 | public function down() 14 | { 15 | $this->dropColumn('{{%menu}}', 'position'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /migrations/m150922_114805_change_entity_id_field.php: -------------------------------------------------------------------------------- 1 | alterColumn('{{%menu_item}}', 'entity_id', Schema::TYPE_STRING.'(50) NOT NULL'); 11 | } 12 | 13 | public function down() 14 | { 15 | echo "m150922_114805_change_entity_id_field cannot be reverted.\n"; 16 | 17 | return false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /migrations/m150929_071512_add_params_column.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%menu_item_lang}}', 'params', Schema::TYPE_TEXT.' NOT NULL'); 11 | } 12 | 13 | public function down() 14 | { 15 | $this->dropColumn('{{%menu_item_lang}}', 'params'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /migrations/m150929_071515_add_type_column.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%menu_item}}', 'type', "ENUM('system','user-defined') NOT NULL DEFAULT 'user-defined'"); 11 | } 12 | 13 | public function down() 14 | { 15 | $this->dropColumn('{{%menu_item}}', 'type'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /models/Menu.php: -------------------------------------------------------------------------------- 1 | 255] 41 | ]; 42 | } 43 | 44 | public function behaviors() 45 | { 46 | return [ 47 | 'timestamp' => [ 48 | 'class' => TimestampBehavior::className(), 49 | 'attributes' => [ 50 | ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], 51 | ActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at', 52 | ], 53 | 'value' => function() { return time(); }, 54 | ], 55 | ]; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function attributeLabels() 62 | { 63 | return [ 64 | 'name' => Yii::t('app', 'Name'), 65 | ]; 66 | } 67 | 68 | /** 69 | * Menu item has children 70 | */ 71 | public static function has_children($parent = NULL) 72 | { 73 | if ($parent === NULL) 74 | return false; 75 | 76 | $q = new Query(); 77 | $result = $q->select('`id`') 78 | ->from(MenuItem::tableName()) 79 | ->where("`parent_id` = {$parent}") 80 | ->count('id'); 81 | 82 | return ($result > 0) ? true : false; 83 | } 84 | 85 | /** 86 | * Render level select html 87 | * 88 | * @param array $settings 89 | * @return string 90 | */ 91 | public static function level_select($settings = []) 92 | { 93 | $default_settings = [ 94 | 'active-only' => FALSE, 95 | 'parent' => 0, 96 | 'relations' => array(), 97 | 'menu-id' => 0, 98 | 'ancestor' => 0, 99 | 'active-ancestor' => 0, 100 | 'selected' => 0, 101 | 'active-menu-item-id' => 0, 102 | ]; 103 | 104 | $settings = array_merge($default_settings, $settings); 105 | 106 | $result = ''; 107 | $items = MenuItem::find()->where(['menu_id' => $settings['menu-id']])->orderby('position ASC')->all(); 108 | 109 | foreach ($items as $item) 110 | { 111 | if ($settings['parent'] == $item->parent_id) 112 | { 113 | ob_start(); 114 | ?> 115 | 124 | id)) 128 | { 129 | $result .= Menu::level_select(array( 130 | 'active-only' => $settings['active-only'], 131 | 'parent' => $item->id, 132 | 'relations' => $settings['relations'], 133 | 'menu-id' => $settings['menu-id'], 134 | 'ancestor' => $item->parent_id, 135 | 'active-ancestor' => $settings['active-ancestor'], 136 | 'selected' => $settings['selected'], 137 | 'active-menu-item-id' => $settings['active-menu-item-id'], 138 | )); 139 | } 140 | } 141 | } 142 | 143 | return $result; 144 | } 145 | 146 | /** 147 | * Get all children by id 148 | * 149 | * @param int $id 150 | * @return array 151 | */ 152 | protected static function findChildren($settings = []) 153 | { 154 | $default_settings = [ 155 | 'id' => 0, 156 | 'ids' => [], 157 | 'menu-id' => 0 158 | ]; 159 | 160 | $settings = array_merge($default_settings, $settings); 161 | 162 | if (!in_array($settings['id'], $settings['ids'])) 163 | $settings['ids'][] = $settings['id']; 164 | 165 | $query = new Query; 166 | 167 | $results = $query->select('id') 168 | ->from(MenuItem::tableName()) 169 | ->where(['parent_id' => $settings['id'], 'menu_id' => $settings['menu-id']]) 170 | ->all(); 171 | 172 | foreach ($results as $result) 173 | { 174 | $settings['ids'][] = $result['id']; 175 | $settings['ids'] = Menu::findChildren([ 176 | 'id' => $result['id'], 177 | 'ids' => $settings['ids'], 178 | 'menu-id' => $settings['menu-id'], 179 | ]); 180 | } 181 | 182 | return $settings['ids']; 183 | } 184 | 185 | /** 186 | * @return \yii\db\ActiveQuery 187 | */ 188 | public function getItems() 189 | { 190 | return $this->hasMany(MenuItem::className(), ['menu_id' => 'id']); 191 | } 192 | 193 | /** 194 | * Recursively deletes all children of the item 195 | * 196 | * @return boolean 197 | */ 198 | public function deleteChildren() 199 | { 200 | foreach ($this->getChildren()->all() as $child) { 201 | if (!$child->delete()) 202 | return false; 203 | } 204 | 205 | return true; 206 | } 207 | 208 | /** 209 | * @return \yii\db\ActiveQuery 210 | */ 211 | public function getChildren() 212 | { 213 | return $this->hasMany(MenuItem::className(), ['menu_id' => 'id']); 214 | } 215 | 216 | /** 217 | * Returns all the children with a specific parent 218 | * @return \yii\db\ActiveQuery 219 | */ 220 | public function getChildrenWithParent($parentId = 0) 221 | { 222 | return $this->getChildren()->where(['parent_id' => $parentId])->orderBy(['position' => SORT_ASC]); 223 | } 224 | 225 | /** 226 | * Returns the current max level 227 | * 228 | * @return int 229 | */ 230 | public function getCurrentMaxLevel() 231 | { 232 | return (new Query()) 233 | ->select('level') 234 | ->from('menu_item') 235 | ->where('menu_id = :menu_id', [':menu_id' => $this->id]) 236 | ->max('level') + 1; 237 | } 238 | 239 | /** 240 | * Returns descendants of the provided parent in a nestable tree structure 241 | * 242 | * @param int $parentId 243 | * @return array 244 | */ 245 | public function getNestableTree($parentId = 0, &$tree = []) 246 | { 247 | // Find the direct descendants of the provided parent 248 | $descendants = $this->getChildrenWithParent($parentId)->all(); 249 | 250 | foreach ($descendants as $k => $descendant) { 251 | $data = ['item' => $descendant]; 252 | 253 | // Load the children of the descendant as a nestable tree 254 | if ($descendant->children) { 255 | $data['children'] = $this->getNestableTree($descendant->id); 256 | } 257 | 258 | $tree[] = $data; 259 | } 260 | 261 | return $tree; 262 | } 263 | 264 | /** 265 | * Returns all items formatted for usage in a Html::dropDownList widget: 266 | * [ 267 | * 'id' => 'name', 268 | * 'id' => 'name, 269 | * ... 270 | * ] 271 | * 272 | * @return array 273 | */ 274 | public function getAllForDropDownList($parent = 0, $language = null) 275 | { 276 | $language = ($language) ?: Yii::$app->language; 277 | 278 | $items = []; 279 | $children = $this->getChildren()->where(['parent_id' => $parent])->orderBy(['position' => SORT_ASC])->all(); 280 | 281 | foreach ($children as $child) { 282 | $items[$child->id] = $child->getTranslation($language)->name; 283 | 284 | if ($child->children) { 285 | $items = $child->getChildrenForDropDownList($items); 286 | } 287 | } 288 | 289 | return $items; 290 | } 291 | 292 | public function getAllForLevelDropDownList() 293 | { 294 | return ArrayHelper::merge([0 => 'Root'], $this->getAllForDropDownList()); 295 | } 296 | } -------------------------------------------------------------------------------- /models/MenuItem.php: -------------------------------------------------------------------------------- 1 | [ 56 | 'class' => TranslateableBehavior::className(), 57 | 'translationAttributes' => ['name', 'params'], 58 | ], 59 | 'timestamp' => [ 60 | 'class' => TimestampBehavior::className(), 61 | 'attributes' => [ 62 | ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'], 63 | ActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at', 64 | ], 65 | 'value' => function() { return time(); }, 66 | ], 67 | ]; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function rules() 74 | { 75 | return [ 76 | [['menu_id', 'active', 'parent_id', 'level', 'position', 'public'], 'integer'], 77 | ['public', 'default', 'value' => Yii::$app->getModule('menu')->defaultPublicVisibility], 78 | [['url', 'anchor', 'type'], 'string', 'max' => 255], 79 | // Required 80 | [['menu_id', 'parent_id', 'entity'], 'required'], 81 | // Only required when the entity is no url 82 | [['entity_id'], 'required', 'when' => function($model) { 83 | return !in_array($model->entity , [self::ENTITY_URL, self::ENTITY_NONE]); 84 | }], 85 | // Trim 86 | [['url', 'anchor'], 'trim'], 87 | [['url'], 'required', 'when' => function($model) { 88 | return $model->entity == self::ENTITY_URL; 89 | }], 90 | [['url'], 'url', 'defaultScheme' => 'http'], 91 | ['active', 'default', 'value' => 1], 92 | [['entity_id'], 'default', 'value' => 0], 93 | ['parent_id', function($attribute, $params) { 94 | if (!empty($this->parent_id) && $this->level > $this->menu->max_level - 1) 95 | $this->addError($attribute, Yii::t('infoweb/menu', 'The maximum level has been reached')); 96 | }] 97 | ]; 98 | } 99 | 100 | /** 101 | * @inheritdoc 102 | */ 103 | public function attributeLabels() 104 | { 105 | return [ 106 | 'menu_id' => Yii::t('infoweb/menu', 'Menu ID'), 107 | 'parent_id' => Yii::t('infoweb/menu', 'Parent ID'), 108 | 'entity' => Yii::t('app', 'Entity'), 109 | 'entity_id' => Yii::t('app', 'Entity ID'), 110 | 'level' => Yii::t('infoweb/menu', 'Level'), 111 | 'name' => Yii::t('app', 'Name'), 112 | 'url' => Yii::t('app', 'Url'), 113 | 'anchor' => Yii::t('infoweb/menu', 'Anchor'), 114 | 'position' => Yii::t('app', 'Position'), 115 | 'active' => Yii::t('app', 'Active'), 116 | 'public' => Yii::t('infoweb/menu', 'Public'), 117 | 'created_at' => Yii::t('app', 'Created At'), 118 | 'updated_at' => Yii::t('app', 'Updated At'), 119 | ]; 120 | } 121 | 122 | /** 123 | * @return \yii\db\ActiveQuery 124 | */ 125 | public function getMenu() 126 | { 127 | return $this->hasOne(Menu::className(), ['id' => 'menu_id']); 128 | } 129 | 130 | /** 131 | * @return \yii\db\ActiveQuery 132 | */ 133 | public function getTranslations() 134 | { 135 | return $this->hasMany(MenuItemLang::className(), ['menu_item_id' => 'id']); 136 | } 137 | 138 | /** 139 | * Returns the model of the entity that is associated with the item 140 | * 141 | * @return mixed 142 | */ 143 | public function getEntityModel() 144 | { 145 | switch ($this->entity) { 146 | 147 | case self::ENTITY_URL: 148 | return self::findOne($this->id); 149 | break; 150 | 151 | default: 152 | $className = $this->entity; 153 | return $className::findOne($this->entity_id); 154 | break; 155 | } 156 | } 157 | 158 | /** 159 | * Returns the url for the item 160 | * 161 | * @param boolean A flag to determine if the language parameter should 162 | * be added to the url 163 | * @param boolean A flag to determine if the url should be prefixed with 164 | * the webpath 165 | * @return string 166 | */ 167 | public function getUrl($includeLanguage = true, $excludeWebPath = false) 168 | { 169 | // Url 170 | if ($this->entity == self::ENTITY_NONE) { 171 | return null; 172 | } elseif ($this->entity == self::ENTITY_URL) { 173 | return $this->url; 174 | } else { 175 | $prefix = (!$excludeWebPath) ? '@web/' : ''; 176 | $prefix .= ($includeLanguage) ? Yii::$app->language.'/' : ''; 177 | 178 | // Page 179 | if ($this->entity == Page::className()) { 180 | $page = $this->getEntityModel(); 181 | 182 | // In the frontend application, the alias for the homepage is ommited 183 | // and '/' is used 184 | if (Yii::$app->id == 'app-frontend' && $page->homepage == true) { 185 | return Url::to($prefix); 186 | } 187 | 188 | $url = "{$prefix}{$page->alias->url}"; 189 | 190 | // Params are set, append to the url 191 | if (!empty($this->params)) { 192 | $url = $url . $this->params; 193 | } 194 | 195 | // An anchor is set, append it to the url 196 | if (!empty($this->anchor)) { 197 | return Url::to("{$url}#{$this->anchor}"); 198 | } 199 | 200 | return Url::to($url); 201 | 202 | // Everything else 203 | } else { 204 | // Second parameter is language 205 | $url = $this->getEntityModel()->getUrl($includeLanguage, null, $excludeWebPath); 206 | 207 | // Params are set, append to the url 208 | if (!empty($this->params)) { 209 | $url = $url . $this->params; 210 | } 211 | 212 | return $url; 213 | } 214 | } 215 | } 216 | 217 | /** 218 | * @return \yii\db\ActiveQuery 219 | */ 220 | public function getChildren() 221 | { 222 | return $this->hasMany(MenuItem::className(), ['parent_id' => 'id']); 223 | } 224 | 225 | /** 226 | * Parent menu item 227 | * @return null|static 228 | */ 229 | public function getParent() 230 | { 231 | return $this->hasOne(MenuItem::className(), ['id' => 'parent_id']); 232 | } 233 | 234 | /** 235 | * Returns a recursive list of all parents of the item 236 | * 237 | * @param int|null The id of the item for which the parents have to be loaded. 238 | * When null is passed, the id of the loaded MenuItem instance is taken. 239 | */ 240 | public function getParents($id = null, $parents = []) 241 | { 242 | if ($id == null) { 243 | $item = $this; 244 | } else { 245 | $item = MenuItem::findOne($id); 246 | } 247 | 248 | if ($item->parent) { 249 | $parents[] = $item->parent; 250 | 251 | return $this->getParents($item->parent->id, $parents); 252 | } 253 | 254 | return $parents; 255 | } 256 | 257 | /** 258 | * Get the next position 259 | * 260 | * @return int 261 | */ 262 | public function nextPosition() 263 | { 264 | $result = (new Query) 265 | ->select('IFNULL(MAX(`position`),0) + 1 AS `position`') 266 | ->from($this->tableName()) 267 | ->where(['level' => $this->level, 'parent_id' => $this->parent_id, 'menu_id' => $this->menu_id]) 268 | ->one(); 269 | 270 | return $result['position']; 271 | } 272 | 273 | /** 274 | * Recursively deletes all children of the item 275 | * 276 | * @return boolean 277 | */ 278 | public function deleteChildren() 279 | { 280 | foreach ($this->getChildren()->all() as $child) { 281 | if (!$child->delete()) 282 | return false; 283 | } 284 | 285 | return true; 286 | } 287 | 288 | /** 289 | * Returns a tree of all items, grouped by menu, formatted for usage in a 290 | * Html::dropDownList widget 291 | * 292 | * @return array 293 | */ 294 | public function getAllForDropDownList($language = null) 295 | { 296 | $language = ($language) ?: Yii::$app->language; 297 | 298 | $items = []; 299 | 300 | foreach (Menu::find()->all() as $menu) { 301 | $items[$menu->name] = $menu->getAllForDropDownList(0, $language); 302 | } 303 | 304 | return $items; 305 | } 306 | 307 | /** 308 | * Returns all children formatted for usage in a Html::dropDownList widget: 309 | * [ 310 | * 'id' => 'name', 311 | * 'id' => 'name, 312 | * ... 313 | * ] 314 | * 315 | * @return array 316 | */ 317 | public function getChildrenForDropDownList($items, $language = null) 318 | { 319 | $language = ($language) ?: Yii::$app->language; 320 | 321 | foreach ($this->getChildren()->orderBy(['position' => SORT_ASC])->all() as $child) { 322 | // Prepend the name for indentation 323 | $prepend = str_repeat('-', ($child->level) ? $child->level * 2 : $child->level); 324 | $prepend .= ($child->level) ? '> ' : ''; 325 | $items[$child->id] = "{$prepend}{$child->getTranslation($language)->name}"; 326 | 327 | if ($child->children) { 328 | $items = $child->getChildrenForDropDownList($items, $language); 329 | } 330 | } 331 | 332 | return $items; 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /models/MenuItemLang.php: -------------------------------------------------------------------------------- 1 | function($model) { 37 | return !$model->isNewRecord; 38 | }], 39 | // Trim 40 | [['name', 'params'], 'trim'], 41 | // Types 42 | [['menu_item_id', 'created_at', 'updated_at'], 'integer'], 43 | [['language'], 'string', 'max' => 10], 44 | [['name'], 'string', 'max' => 255], 45 | [['params'], 'string'], 46 | ]; 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function attributeLabels() 53 | { 54 | return [ 55 | 'id' => Yii::t('app', 'ID'), 56 | 'menu_item_id' => Yii::t('infoweb/menu', 'Menu ID'), 57 | 'language' => Yii::t('app', 'Language'), 58 | 'name' => Yii::t('app', 'Name'), 59 | ]; 60 | } 61 | 62 | /** 63 | * @return \yii\db\ActiveQuery 64 | */ 65 | public function getMenu() 66 | { 67 | return $this->hasOne(MenuItem::className(), ['id' => 'menu_item_id']); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /models/MenuItemSearch.php: -------------------------------------------------------------------------------- 1 | getModule('menu')->enablePrivateMenuItems) 23 | $integerFields[] = 'public'; 24 | 25 | return [ 26 | [$integerFields, 'integer'], 27 | [['entity', 'name', 'url'], 'safe'], 28 | ]; 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function scenarios() 35 | { 36 | // bypass scenarios() implementation in the parent class 37 | return Model::scenarios(); 38 | } 39 | 40 | /** 41 | * Creates data provider instance with search query applied 42 | * 43 | * @param array $params 44 | * 45 | * @return ActiveDataProvider 46 | */ 47 | public function search($params) 48 | { 49 | $query = MenuItem::find(); 50 | 51 | $dataProvider = new ActiveDataProvider([ 52 | 'query' => $query, 53 | ]); 54 | 55 | if (!($this->load($params) && $this->validate())) { 56 | return $dataProvider; 57 | } 58 | 59 | $query->andFilterWhere([ 60 | 'id' => $this->id, 61 | 'menu_id' => $this->menu_id, 62 | 'parent_id' => $this->parent_id, 63 | 'entity_id' => $this->entity_id, 64 | 'level' => $this->level, 65 | 'position' => $this->position, 66 | 'active' => $this->active, 67 | 'created_at' => $this->created_at, 68 | 'updated_at' => $this->updated_at, 69 | ]); 70 | 71 | $query->andFilterWhere(['like', 'entity', $this->entity]) 72 | ->andFilterWhere(['like', 'name', $this->name]) 73 | ->andFilterWhere(['like', 'url', $this->url]); 74 | 75 | if (Yii::$app->getModule('menu')->enablePrivateMenuItems) 76 | $query->andFilterWhere(['public' => $this->public]); 77 | 78 | return $dataProvider; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /models/MenuSearch.php: -------------------------------------------------------------------------------- 1 | $query, 48 | 'sort' => ['defaultOrder' => ['position' => SORT_ASC]], 49 | ]); 50 | 51 | if (!($this->load($params) && $this->validate())) { 52 | return $dataProvider; 53 | } 54 | 55 | $query->andFilterWhere([ 56 | 'id' => $this->id, 57 | 'created_at' => $this->created_at, 58 | 'updated_at' => $this->updated_at, 59 | ]); 60 | 61 | $query->andFilterWhere(['like', 'name', $this->name]); 62 | 63 | return $dataProvider; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /models/frontend/Menu.php: -------------------------------------------------------------------------------- 1 | where(['active' => 1]); 20 | 21 | // Filter by parent 22 | if (isset($options['parentId'])) 23 | $items = $items->andWhere(['parent_id' => $options['parentId']]); 24 | 25 | // Filter by level 26 | if (isset($options['level'])) { 27 | 28 | $items = $items->andWhere(['level' => $options['level']]); 29 | } 30 | 31 | // Only show public items to guest users if this options is enabled in the "menu" module 32 | if (Yii::$app->getModule('menu') && Yii::$app->getModule('menu')->enablePrivateMenuItems && Yii::$app->user->isGuest) 33 | $items = $items->andWhere(['public' => 1]); 34 | 35 | return $items->orderBy('position', 'ASC'); 36 | } 37 | 38 | /** 39 | * Returns a tree of menu-items that belong to the menu 40 | * 41 | * @param array $settings A settings array that holds the parent-id and level 42 | * @return array 43 | */ 44 | public function getTree($settings = ['subMenu' => true, 'parentId' => 0, 'level' => 0, 'includeLanguage' => true, 'convertBr' => false]) 45 | { 46 | $items = []; 47 | $menuItems = $this->getItems($settings)->all(); 48 | 49 | foreach ($menuItems as $menuItem) { 50 | 51 | // First we need to check if the item has a non-public page attached 52 | // If so, and no user is logged in, the item is skipped 53 | if ($menuItem->entity == Page::className() && Yii::$app->user->isGuest) { 54 | $menuItemEntity = $menuItem->entityModel; 55 | if (isset($menuItemEntity->public) && $menuItemEntity->public == false) { 56 | continue; 57 | } 58 | } 59 | 60 | $item = [ 61 | 'label' => (isset($settings['convertBr'])) ? str_replace("\n", 'Naam | 10 |Acties | 11 |
---|---|
16 |
17 | render('_nestableList', ['items' => $items, 'privateItemsEnabled' => $privateItemsEnabled]); ?>
18 |
19 | |
20 |