├── .gitignore ├── screenshot.png ├── src ├── Facades │ └── Menu.php ├── Models │ ├── Traits │ │ └── QueryCacheTrait.php │ ├── Menus.php │ └── MenuItems.php ├── Events │ ├── CreatedMenuEvent.php │ ├── DestroyMenuEvent.php │ └── UpdatedMenuEvent.php ├── Providers │ └── MenuServiceProvider.php ├── WMenu.php ├── Http │ └── Controllers │ │ └── MenuController.php └── Database │ └── Query │ └── CacheableQueryBuilder.php ├── resources └── views │ ├── scripts.blade.php │ ├── menu-html.blade.php │ ├── partials │ ├── left.blade.php │ ├── right.blade.php │ └── loop-item.blade.php │ └── accordions │ ├── add-link.blade.php │ └── default.blade.php ├── database └── migrations │ ├── 2019_01_05_293551_add-role-id-to-menu-items-table.php │ ├── 2022_07_06_000123_add_class_to_menu_table.php │ ├── 2017_08_11_073824_create_menus_wp_table.php │ └── 2017_08_11_074006_create_menu_items_wp_table.php ├── composer.json ├── LICENSE.md ├── routes └── web.php ├── config └── menu.php ├── public ├── style.css └── menu.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyendachuy/laravel-menu/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/Facades/Menu.php: -------------------------------------------------------------------------------- 1 | getConnection(); 18 | return new CacheableQueryBuilder( 19 | $connection, 20 | $connection->getQueryGrammar(), 21 | $connection->getPostProcessor(), 22 | get_class($this) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/views/scripts.blade.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /database/migrations/2019_01_05_293551_add-role-id-to-menu-items-table.php: -------------------------------------------------------------------------------- 1 | integer('role_id')->default(0); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table(config('menu.table_prefix') . config('menu.table_name_items'), function ($table) { 29 | $table->dropColumn('role_id'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_07_06_000123_add_class_to_menu_table.php: -------------------------------------------------------------------------------- 1 | string('class')->nullable()->after('name'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table(config('menu.table_prefix') . config('menu.table_name_menus'), function ($table) { 29 | $table->dropColumn('class'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nguyendachuy/laravel-menu", 3 | "description": "Laravel Menu Builder | Drag & Drop | Bootstrap | Laravel 7 | Laravel 8 | Laravel 9 | Laravel 10 | Laravel 11", 4 | "keywords": [ 5 | "Laravel", 6 | "Menu", 7 | "Bootstrap", 8 | "Builder", 9 | "Drag", 10 | "Drop" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Nguyen Dac Huy", 15 | "email": "huy.cit@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.2.5", 21 | "illuminate/support": "7.* || 8.* || 9.* || 10.* || 11.*" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "NguyenHuy\\Menu\\": "src" 26 | } 27 | }, 28 | "extra": { 29 | "laravel": { 30 | "providers": [ 31 | "NguyenHuy\\Menu\\Providers\\MenuServiceProvider" 32 | ], 33 | "aliases": { 34 | "Menu": "NguyenHuy\\Menu\\Facades\\Menu" 35 | } 36 | } 37 | }, 38 | "minimum-stability": "dev", 39 | "prefer-stable": true 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/Menus.php: -------------------------------------------------------------------------------- 1 | table = config('menu.table_prefix') . config('menu.table_name_menus'); 17 | } 18 | 19 | public static function byName($name) 20 | { 21 | return self::where('name', '=', $name)->first(); 22 | } 23 | 24 | public function items() 25 | { 26 | return $this->hasMany('NguyenHuy\Menu\Models\MenuItems', 'menu') 27 | ->with('child') 28 | ->where('parent', 0) 29 | ->orderBy('sort', 'ASC'); 30 | } 31 | public function itemAndChilds() 32 | { 33 | return $this->hasMany('NguyenHuy\Menu\Models\MenuItems', 'menu') 34 | ->with('child') 35 | ->orderBy('sort', 'ASC'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2017_08_11_073824_create_menus_wp_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 19 | $table->string('name'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists(config('menu.table_prefix') . config('menu.table_name_menus')); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Events/CreatedMenuEvent.php: -------------------------------------------------------------------------------- 1 | data = $data; 29 | } 30 | 31 | /** 32 | * Get the channels the event should broadcast on. 33 | * 34 | * @return \Illuminate\Broadcasting\Channel|array 35 | */ 36 | public function broadcastOn() 37 | { 38 | return new PrivateChannel('channel-name'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Events/DestroyMenuEvent.php: -------------------------------------------------------------------------------- 1 | data = $data; 29 | } 30 | 31 | /** 32 | * Get the channels the event should broadcast on. 33 | * 34 | * @return \Illuminate\Broadcasting\Channel|array 35 | */ 36 | public function broadcastOn() 37 | { 38 | return new PrivateChannel('channel-name'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Events/UpdatedMenuEvent.php: -------------------------------------------------------------------------------- 1 | data = $data; 29 | } 30 | 31 | /** 32 | * Get the channels the event should broadcast on. 33 | * 34 | * @return \Illuminate\Broadcasting\Channel|array 35 | */ 36 | public function broadcastOn() 37 | { 38 | return new PrivateChannel('channel-name'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 All contributors from GitHub 4 | Copyright (c) 2021 NguyenDacHuy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'NguyenHuy\Menu\Http\Controllers', 5 | 'middleware' => config('menu.middleware'), 6 | 'prefix' => config('menu.route_prefix'), 7 | 'as' => 'h-menu.', 8 | ], function () { 9 | /** 10 | * menu items 11 | */ 12 | Route::post('add-item', array( 13 | 'as' => 'add-item', 14 | 'uses' => 'MenuController@createItem' 15 | )); 16 | Route::post('delete-item', array( 17 | 'as' => 'delete-item', 18 | 'uses' => 'MenuController@destroyItem' 19 | )); 20 | Route::post('update-item', array( 21 | 'as' => 'update-item', 22 | 'uses' => 'MenuController@updateItem' 23 | )); 24 | 25 | /** 26 | * menu 27 | */ 28 | Route::post('create-menu', array( 29 | 'as' => 'create-menu', 30 | 'uses' => 'MenuController@createNewMenu' 31 | )); 32 | Route::post('delete-menu', array( 33 | 'as' => 'delete-menu', 34 | 'uses' => 'MenuController@destroyMenu' 35 | )); 36 | Route::post('update-menu-and-items', array( 37 | 'as' => 'update-menu-and-items', 38 | 'uses' => 'MenuController@generateMenuControl' 39 | )); 40 | }); 41 | -------------------------------------------------------------------------------- /src/Models/MenuItems.php: -------------------------------------------------------------------------------- 1 | table = config('menu.table_prefix') . config('menu.table_name_items'); 23 | } 24 | 25 | public function getsons($id) 26 | { 27 | return $this->where('parent', $id)->get(); 28 | } 29 | public function getall($id) 30 | { 31 | return $this->where('menu', $id)->orderBy('sort', 'asc')->get(); 32 | } 33 | 34 | public static function getNextSortRoot($menu) 35 | { 36 | return self::where('menu', $menu)->max('sort') + 1; 37 | } 38 | 39 | public function parent_menu() 40 | { 41 | return $this->belongsTo('NguyenHuy\Menu\Models\Menus', 'menu'); 42 | } 43 | 44 | public function child() 45 | { 46 | return $this->hasMany('NguyenHuy\Menu\Models\MenuItems', 'parent')->orderBy('sort', 'ASC'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/views/menu-html.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $currentUrl = url()->current(); 3 | @endphp 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | {!! Menu::select('menu', $menulist, ['class' => 'form-control']) !!} 13 | 14 |
15 | or Create New Menu 16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 | @include('nguyendachuy-menu::partials.left') 25 |
26 | {{-- /col-md-4 --}} 27 |
28 | @include('nguyendachuy-menu::partials.right') 29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /config/menu.php: -------------------------------------------------------------------------------- 1 | [], 15 | 16 | /* you can set your own table prefix here */ 17 | 'table_prefix' => 'admin_', 18 | 19 | /* you can set your own table names */ 20 | 'table_name_menus' => 'menus', 21 | 22 | 'table_name_items' => 'menu_items', 23 | 24 | /* you can set your route path*/ 25 | 'route_prefix' => 'nguyendachuy', 26 | 27 | /* here you can make menu items visible to specific roles */ 28 | 'use_roles' => false, 29 | 30 | /* If use_roles = true above then must set the table name, primary key and title field to get roles details */ 31 | 'roles_table' => 'roles', 32 | 33 | 'roles_pk' => 'id', // primary key of the roles table 34 | 35 | 'roles_title_field' => 'name', // display name (field) of the roles table 36 | 37 | /** 38 | * Cache configuration 39 | */ 40 | 'cache' => [ 41 | 'enabled' => false, // enable or disable cache 42 | 'minutes' => 60, // cache time in minutes (default: 60) 43 | 'prefix' => 'menu', // prefix for cache key 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /database/migrations/2017_08_11_074006_create_menu_items_wp_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 19 | $table->string('label'); 20 | $table->string('link'); 21 | $table->string('icon')->nullable(); 22 | $table->unsignedBigInteger('parent')->default(0); 23 | $table->integer('sort')->default(0); 24 | $table->string('class')->nullable(); 25 | $table->enum('target', ['_self', '_blank'])->default('_self'); 26 | $table->unsignedBigInteger('menu'); 27 | $table->integer('depth')->default(0); 28 | $table->timestamps(); 29 | 30 | $table->foreign('menu')->references('id')->on(config('menu.table_prefix') . config('menu.table_name_menus')) 31 | ->onDelete('cascade') 32 | ->onUpdate('cascade'); 33 | }); 34 | } 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | Schema::dropIfExists(config('menu.table_prefix') . config('menu.table_name_items')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /resources/views/partials/left.blade.php: -------------------------------------------------------------------------------- 1 | @if(!empty(request()->get('menu'))) 2 |
3 | @php 4 | // $blogs = \App\Blog::get(['id', 'title'])->map(function($blog){ 5 | // return [ 6 | // 'url' => $blog->getLink(), 7 | // 'icon' => '', 8 | // 'label' => $blog->title, 9 | // ]; 10 | // }); 11 | $pages = [ 12 | [ 13 | 'url' => '/page1', 14 | 'icon' => '', 15 | 'label' => 'Page 1', 16 | ], 17 | [ 18 | 'url' => '/page2', 19 | 'icon' => '', 20 | 'label' => 'Page 2', 21 | ], 22 | [ 23 | 'url' => '/page3', 24 | 'icon' => '', 25 | 'label' => 'Page 2', 26 | ], 27 | [ 28 | 'url' => '/page4', 29 | 'icon' => '', 30 | 'label' => 'Page 4', 31 | ], 32 | [ 33 | 'url' => '/page5', 34 | 'icon' => '', 35 | 'label' => 'Page 5', 36 | ] 37 | ]; 38 | @endphp 39 | @include('nguyendachuy-menu::accordions.default', [ 40 | 'name' => 'Pages', 41 | 'urls' => $pages, 42 | 'show' => true 43 | ]) 44 | @php 45 | $categories = [ 46 | [ 47 | 'url' => '/category1', 48 | 'icon' => '', 49 | 'label' => 'Category 1', 50 | ], 51 | [ 52 | 'url' => '/category2', 53 | 'icon' => '', 54 | 'label' => 'Category 2', 55 | ], 56 | [ 57 | 'url' => '/category3', 58 | 'icon' => '', 59 | 'label' => 'Category 2', 60 | ], 61 | [ 62 | 'url' => '/category4', 63 | 'icon' => '', 64 | 'label' => 'Category 4', 65 | ], 66 | [ 67 | 'url' => '/category5', 68 | 'icon' => '', 69 | 'label' => 'Category 5', 70 | ] 71 | ]; 72 | @endphp 73 | @include('nguyendachuy-menu::accordions.default', ['name' => 'Categories', 'urls' => $categories]) 74 | 75 | @include('nguyendachuy-menu::accordions.add-link', ['name' => 'Add Link']) 76 |
77 | @endif -------------------------------------------------------------------------------- /resources/views/accordions/add-link.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $id = rand(100000, 999999); 3 | @endphp 4 |
5 |
6 |
7 | 13 |
14 |
15 | 16 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | Ex: <span class="oi oi-align-center"></span> 34 | 35 |
36 | @if(!empty($roles)) 37 |
38 | 39 | 47 |
48 | @endif 49 |
50 | 54 |
55 |
56 |
57 |
58 |
-------------------------------------------------------------------------------- /src/Providers/MenuServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->routesAreCached()) { 18 | require __DIR__ . '/../../routes/web.php'; 19 | } 20 | 21 | $this->loadViewsFrom(__DIR__ . '/../../../', 'nguyendachuy-menu'); 22 | 23 | $this->publishes([ 24 | __DIR__ . '/../../config/menu.php' => config_path('menu.php'), 25 | ], 'laravel-menu-config'); 26 | 27 | $this->publishes([ 28 | __DIR__ . '/../../resources/views' => resource_path('views/vendor/nguyendachuy-menu'), 29 | ], 'laravel-menu-view'); 30 | 31 | $this->publishes([ 32 | __DIR__ . '/../../public' => public_path('vendor/nguyendachuy-menu'), 33 | ], 'laravel-menu-public'); 34 | 35 | $this->publishes([ 36 | __DIR__ . '/../../database/migrations/2017_08_11_073824_create_menus_wp_table.php' 37 | => database_path('migrations/2017_08_11_073824_create_menus_wp_table.php'), 38 | __DIR__ . '/../../database/migrations/2017_08_11_074006_create_menu_items_wp_table.php' 39 | => database_path('migrations/2017_08_11_074006_create_menu_items_wp_table.php'), 40 | __DIR__ . '/../../database/migrations/2019_01_05_293551_add-role-id-to-menu-items-table.php' 41 | => database_path('2019_01_05_293551_add-role-id-to-menu-items-table.php'), 42 | __DIR__ . '/../../database/migrations/2022_07_06_000123_add_class_to_menu_table.php' 43 | => database_path('migrations/2022_07_15_000123_add_class_to_menu_table.php'), 44 | ], 'laravel-menu-migrations'); 45 | } 46 | 47 | /** 48 | * Register the application services. 49 | * 50 | * @return void 51 | */ 52 | public function register() 53 | { 54 | $this->app->bind('nguyendachuy-menu', function () { 55 | return new WMenu(); 56 | }); 57 | 58 | $this->app->make('NguyenHuy\Menu\Http\Controllers\MenuController'); 59 | $this->mergeConfigFrom( 60 | __DIR__ . '/../../config/menu.php', 61 | 'menu' 62 | ); 63 | } 64 | protected function migrationExists($mgr) 65 | { 66 | $path = database_path('migrations/'); 67 | $files = scandir($path); 68 | $pos = false; 69 | foreach ($files as &$value) { 70 | $pos = strpos($value, $mgr); 71 | if ($pos !== false) return true; 72 | } 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/views/accordions/default.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $id = rand(100000, 999999); 3 | @endphp 4 |
5 |
6 |
7 | 13 |
14 |
15 | 16 |
19 | 71 |
72 |
-------------------------------------------------------------------------------- /src/WMenu.php: -------------------------------------------------------------------------------- 1 | select(['id', 'name'])->get(); 16 | $menulist = $menulist->pluck('name', 'id')->prepend('Select menu', 0)->all(); 17 | 18 | //$roles = Role::all(); 19 | 20 | if ( 21 | (request()->has('action') && empty(request()->input('menu'))) 22 | || request()->input('menu') == '0' 23 | ) { 24 | return view('nguyendachuy-menu::menu-html')->with("menulist", $menulist); 25 | } else { 26 | $menu = Menus::find(request()->input('menu')); 27 | $menus = self::get(request()->input('menu')); 28 | $data = ['menus' => $menus, 'indmenu' => $menu, 'menulist' => $menulist]; 29 | if (config('menu.use_roles')) { 30 | $data['roles'] = DB::table(config('menu.roles_table'))->select([ 31 | config('menu.roles_pk'), 32 | config('menu.roles_title_field') 33 | ]) 34 | ->get(); 35 | $data['role_pk'] = config('menu.roles_pk'); 36 | $data['role_title_field'] = config('menu.roles_title_field'); 37 | } 38 | return view('nguyendachuy-menu::menu-html', $data); 39 | } 40 | } 41 | 42 | public function scripts() 43 | { 44 | return view('nguyendachuy-menu::scripts'); 45 | } 46 | 47 | public function select($name = "menu", $menulist = array(), $attributes = array()) 48 | { 49 | $attribute_string = ""; 50 | if (count($attributes) > 0) { 51 | $attribute_string = str_replace( 52 | "=", 53 | '="', 54 | http_build_query($attributes, '', '" ', PHP_QUERY_RFC3986) 55 | ) . '"'; 56 | } 57 | $html = ''; 66 | return $html; 67 | } 68 | 69 | 70 | /** 71 | * Returns empty array if menu not found now. 72 | * Thanks @sovichet 73 | * 74 | * @param $name 75 | * @return array 76 | */ 77 | public static function getByName($name) 78 | { 79 | $menu = Menus::byName($name); 80 | return is_null($menu) ? [] : self::get($menu->id); 81 | } 82 | 83 | public static function get($menu_id) 84 | { 85 | $menuItem = new MenuItems; 86 | $menu_list = $menuItem->getall($menu_id); 87 | 88 | $roots = $menu_list->where('menu', (int) $menu_id)->where('parent', 0); 89 | 90 | $items = self::tree($roots, $menu_list); 91 | return $items; 92 | } 93 | 94 | private static function tree($items, $all_items) 95 | { 96 | $data_arr = array(); 97 | $i = 0; 98 | foreach ($items as $item) { 99 | $data_arr[$i] = $item->toArray(); 100 | $find = $all_items->where('parent', $item->id); 101 | 102 | $data_arr[$i]['child'] = array(); 103 | 104 | if ($find->count()) { 105 | $data_arr[$i]['child'] = self::tree($find, $all_items); 106 | } 107 | 108 | $i++; 109 | } 110 | 111 | return $data_arr; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /resources/views/partials/right.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 | 8 | 11 | @if(request()->has('action')) 12 | 14 | @elseif(request()->has('menu')) 15 | 17 | @else 18 | 20 | @endif 21 |
22 |
23 |
24 |
25 |
26 | @if(request()->get('menu') != 0 && isset($menus) && count($menus) > 0) 27 |
28 |
29 |

Menu Structure

30 |

Place each item in the order you prefer. Click to the right of the item to display more configuration options.

31 |
32 |
33 | @elseif(request()->get('menu') == 0) 34 |
35 |
36 |

Menu Creation

37 |

Please enter the name and select "Create menu" button

38 |
39 |
40 | @else 41 |
42 |
43 |

Create Menu Item

44 |

45 |
46 |
47 | @endif 48 | 49 |
50 | @if(isset($menus) && count($menus) > 0) 51 |
52 |
    53 | @foreach($menus as $key => $m) 54 | @include('nguyendachuy-menu::partials.loop-item', ['key' => $key]) 55 | @endforeach 56 |
57 |
58 | @endif 59 |
60 |
61 |
62 |
63 | @if(request()->get('menu') != 0) 64 | 74 | @endif 75 |
-------------------------------------------------------------------------------- /src/Http/Controllers/MenuController.php: -------------------------------------------------------------------------------- 1 | name = $request->input('name'); 19 | $menu->class = $request->input('class', null); 20 | $menu->save(); 21 | 22 | event(new CreatedMenuEvent($menu)); 23 | 24 | return response()->json([ 25 | 'resp' => $menu->id 26 | ], 200); 27 | } 28 | 29 | public function destroyMenu(Request $request) 30 | { 31 | $menudelete = Menus::findOrFail($request->input('id')); 32 | $menudelete->delete(); 33 | 34 | event(new DestroyMenuEvent($menudelete)); 35 | 36 | return response()->json([ 37 | 'resp' => 'You delete this item' 38 | ], 200); 39 | } 40 | 41 | public function generateMenuControl(Request $request) 42 | { 43 | $menu = Menus::findOrFail($request->input('idMenu')); 44 | $menu->name = $request->input('menuName'); 45 | $menu->class = $request->input('class', null); 46 | $menu->save(); 47 | if (is_array($request->input('data'))) { 48 | foreach ($request->input('data') as $key => $value) { 49 | $menuitem = MenuItems::findOrFail($value['id']); 50 | $menuitem->parent = $value['parent_id'] ?? 0; 51 | $menuitem->sort = $key; 52 | $menuitem->depth = $value['depth'] ?? 1; 53 | if (config('menu.use_roles')) { 54 | $menuitem->role_id = $request->input('role_id'); 55 | } 56 | $menuitem->save(); 57 | } 58 | } 59 | return response()->json([ 60 | 'resp' => 1 61 | ], 200); 62 | } 63 | 64 | public function createItem(Request $request) 65 | { 66 | if ($request->has('data')) { 67 | foreach ($request->post('data') as $key => $value) { 68 | $menuitem = new MenuItems(); 69 | $menuitem->label = $value['label']; 70 | $menuitem->link = $value['url']; 71 | $menuitem->icon = $value['icon']; 72 | if (config('menu.use_roles')) { 73 | $menuitem->role_id = $value['role'] ?? 0; 74 | } 75 | $menuitem->menu = $value['id']; 76 | $menuitem->sort = MenuItems::getNextSortRoot($value['id']); 77 | $menuitem->save(); 78 | } 79 | } 80 | 81 | return response()->json([ 82 | 'resp' => 1 83 | ], 200); 84 | } 85 | 86 | public function updateItem(Request $request) 87 | { 88 | $dataItem = $request->input('dataItem'); 89 | if (is_array($dataItem)) { 90 | foreach ($dataItem as $value) { 91 | $menuitem = MenuItems::findOrFail($value['id']); 92 | $menuitem->label = $value['label']; 93 | $menuitem->link = $value['link']; 94 | $menuitem->class = $value['class']; 95 | $menuitem->icon = $value['icon']; 96 | $menuitem->target = $value['target']; 97 | if (config('menu.use_roles')) { 98 | $menuitem->role_id = $value['role_id'] ? $value['role_id'] : 0; 99 | } 100 | $menuitem->save(); 101 | } 102 | } else { 103 | $menuitem = MenuItems::findOrFail($request->input('id')); 104 | $menuitem->label = $request->input('label'); 105 | $menuitem->link = $request->input('url'); 106 | $menuitem->class = $request->input('clases'); 107 | $menuitem->icon = $request->input('icon'); 108 | $menuitem->target = $request->input('target'); 109 | if (config('menu.use_roles')) { 110 | $menuitem->role_id = $request->input('role_id') ? $request->input('role_id') : 0; 111 | } 112 | $menuitem->save(); 113 | } 114 | 115 | event(new UpdatedMenuEvent($dataItem)); 116 | 117 | return response()->json([ 118 | 'resp' => 1 119 | ], 200); 120 | } 121 | 122 | public function destroyItem(Request $request) 123 | { 124 | $menuitem = MenuItems::findOrFail($request->input('id')); 125 | $menuitem->delete(); 126 | 127 | return response()->json([ 128 | 'resp' => 1 129 | ], 200); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /resources/views/partials/loop-item.blade.php: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 | {{$m['label']}} 7 | 8 | / 9 | 10 | {{ \Illuminate\Support\Str::limit($m['link'], $limit = 30, $end = '...') }} 11 | 12 | |{{$m['id']}}| 13 | 14 | 19 |
    20 | {{-- @if($key == 0) show @endif --}} 21 |
    22 |
    23 | 80 |
    81 |
    82 | @if (isset($m['child']) && count($m['child']) > 0) 83 |
      84 | @foreach($m['child'] as $_m) 85 | @include('nguyendachuy-menu::partials.loop-item', ['m' => $_m, 'key' => 1]) 86 | @endforeach 87 |
    88 | @endif 89 |
  • 90 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | #nguyen-huy ul { 2 | list-style: none; 3 | } 4 | 5 | #nguyen-huy .menu-item-handle { 6 | cursor: move; 7 | } 8 | 9 | #nguyen-huy .menu-item-handle .menu-item-title.no-title { 10 | color: #999; 11 | } 12 | 13 | #nguyen-huy #nestable .card { 14 | border: none; 15 | } 16 | 17 | #nguyen-huy a, 18 | #nguyen-huy .btn-link { 19 | color: rgb(15, 63, 196); 20 | } 21 | 22 | #nguyen-huy a:hover, 23 | #nguyen-huy .btn-link { 24 | text-decoration: none; 25 | } 26 | 27 | #nguyen-huy #accordion-left .card-header { 28 | padding: 0px; 29 | } 30 | 31 | #nguyen-huy #accordion-left .card { 32 | margin-top: 10px; 33 | } 34 | 35 | #nguyen-huy #accordion-left .btn-link { 36 | width: 100%; 37 | text-align: left; 38 | } 39 | 40 | #nguyen-huy .card-header { 41 | background-color: #e9ecef; 42 | } 43 | /** 44 | Fixes for boostrap 4.3.1*/ 45 | #nguyen-huy .card-header { 46 | display: block; 47 | } 48 | 49 | #nguyen-huy .jumbotron .container { 50 | padding-top: 5px; 51 | } 52 | 53 | #accordion-left .collapsed .narrow-icon:before { 54 | content: "\f107" 55 | } 56 | 57 | #accordion-left button[aria-expanded=true] .narrow-icon:before, 58 | #nguyen-huy div[aria-expanded=true] .narrow-icon:before { 59 | content: "\f106" 60 | } 61 | /** 62 | * Nestable 63 | */ 64 | .dd-list { 65 | display: block; 66 | position: relative; 67 | margin: 0; 68 | padding: 0; 69 | list-style: none; 70 | } 71 | 72 | .dd-list .dd-list { 73 | padding-left: 30px; 74 | margin-top: 5px; 75 | } 76 | 77 | .dd-collapsed .dd-list { 78 | display: none; 79 | } 80 | 81 | .dd-item, 82 | .dd-empty, 83 | .dd-placeholder { 84 | display: block; 85 | position: relative; 86 | margin: 0; 87 | padding: 0; 88 | min-height: 20px; 89 | font-size: 13px; 90 | line-height: 20px; 91 | } 92 | 93 | .dd-handle { 94 | cursor: move; 95 | color: #333; 96 | text-decoration: none; 97 | background: #fafafa; 98 | padding: 13px; 99 | margin-left: -20px; 100 | } 101 | 102 | .dd-handle:hover { 103 | color: #2ea8e5; 104 | background: #fff; 105 | } 106 | 107 | .dd-item>button { 108 | display: block; 109 | position: relative; 110 | cursor: pointer; 111 | float: left; 112 | width: 25px; 113 | height: 20px; 114 | margin: 7px 0; 115 | padding: 0; 116 | text-indent: 100%; 117 | white-space: nowrap; 118 | overflow: hidden; 119 | border: 0; 120 | background: transparent; 121 | font-size: 10px; 122 | line-height: 1; 123 | text-align: center; 124 | font-weight: bold; 125 | } 126 | 127 | .dd-item>button:before { 128 | content: '\f067'; 129 | display: block; 130 | position: absolute; 131 | width: 100%; 132 | text-align: center; 133 | text-indent: 0; 134 | font-family: 'FontAwesome' 135 | } 136 | 137 | .dd-item>button[data-action="collapse"]:before { 138 | content: '\f068'; 139 | } 140 | 141 | .dd-placeholder, 142 | .dd-empty { 143 | margin: 5px 0; 144 | padding: 0; 145 | min-height: 30px; 146 | background: #f2fbff; 147 | border: 1px dashed #b6bcbf; 148 | box-sizing: border-box; 149 | -moz-box-sizing: border-box; 150 | } 151 | 152 | .dd-empty { 153 | border: 1px dashed #bbb; 154 | min-height: 100px; 155 | background-color: #e5e5e5; 156 | background-size: 60px 60px; 157 | background-position: 0 0, 30px 30px; 158 | } 159 | 160 | .dd-dragel { 161 | position: absolute; 162 | pointer-events: none; 163 | z-index: 9999; 164 | } 165 | 166 | .dd-dragel>.dd-item .dd-handle { 167 | margin-top: 0; 168 | } 169 | 170 | .dd-dragel .dd-handle { 171 | -webkit-box-shadow: 2px 4px 6px 0 rgba(0, 0, 0, .1); 172 | box-shadow: 2px 4px 6px 0 rgba(0, 0, 0, .1); 173 | } 174 | 175 | .dd-hover>.dd-handle { 176 | background: #2ea8e5 !important; 177 | } 178 | 179 | /** 180 | * Nestable Draggable Handles 181 | */ 182 | 183 | .dd3-content { 184 | display: block; 185 | height: 30px; 186 | margin: 5px 0; 187 | padding: 5px 10px 5px 40px; 188 | color: #333; 189 | text-decoration: none; 190 | font-weight: 400; 191 | border: 1px solid #ccc; 192 | background: #fafafa; 193 | -webkit-border-radius: 3px; 194 | border-radius: 3px; 195 | box-sizing: border-box; 196 | -moz-box-sizing: border-box; 197 | } 198 | 199 | .dd3-content:hover { 200 | color: #2ea8e5; 201 | background: #fff; 202 | } 203 | 204 | .dd-dragel>.dd3-item>.dd3-content { 205 | margin: 0; 206 | } 207 | 208 | .dd3-item>button { 209 | margin-left: 30px; 210 | } 211 | 212 | .dd3-handle { 213 | position: absolute; 214 | margin: 0; 215 | left: 0; 216 | top: 0; 217 | cursor: move; 218 | width: 30px; 219 | text-indent: 100%; 220 | white-space: nowrap; 221 | overflow: hidden; 222 | border: 1px solid #aaa; 223 | background: #ddd; 224 | border-top-right-radius: 0; 225 | border-bottom-right-radius: 0; 226 | } 227 | 228 | .dd3-handle:before { 229 | content: '≡'; 230 | display: block; 231 | position: absolute; 232 | left: 0; 233 | top: 3px; 234 | width: 100%; 235 | text-align: center; 236 | text-indent: 0; 237 | color: #fff; 238 | font-size: 20px; 239 | font-weight: normal; 240 | } 241 | 242 | .dd3-handle:hover { 243 | background: #ddd; 244 | } 245 | 246 | /* 247 | * loading 248 | */ 249 | .ajax-loader { 250 | position: fixed; 251 | top: 50%; 252 | left: 50%; 253 | display: none; 254 | z-index: 9999989; 255 | } 256 | 257 | .lds-ripple { 258 | display: inline-block; 259 | position: relative; 260 | width: 80px; 261 | height: 80px; 262 | } 263 | 264 | .lds-ripple div { 265 | position: absolute; 266 | border: 4px solid #da3ad8; 267 | opacity: 1; 268 | border-radius: 50%; 269 | animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; 270 | } 271 | 272 | .lds-ripple div:nth-child(2) { 273 | animation-delay: -0.5s; 274 | } 275 | 276 | @keyframes lds-ripple { 277 | 0% { 278 | top: 36px; 279 | left: 36px; 280 | width: 0; 281 | height: 0; 282 | opacity: 1; 283 | } 284 | 285 | 100% { 286 | top: 0px; 287 | left: 0px; 288 | width: 72px; 289 | height: 72px; 290 | opacity: 0; 291 | } 292 | } 293 | .box-links-for-menu { 294 | padding: 0px; 295 | } 296 | 297 | .box-links-for-menu .list-item { 298 | border: 1px solid #ddd; 299 | max-height: 200px; 300 | overflow: auto; 301 | padding: 15px 302 | } 303 | 304 | .box-links-for-menu .list-item .list-item { 305 | border: 0; 306 | max-height: none; 307 | overflow: visible; 308 | padding: 0 0 0 20px 309 | } 310 | 311 | .box-links-for-menu .list-item li { 312 | list-style: none; 313 | margin-bottom: 5px; 314 | position: relative 315 | } 316 | 317 | .box-links-for-menu .list-item li ul { 318 | padding-left: 20px 319 | } 320 | 321 | .box-links-for-menu .list-item li .checker { 322 | margin-top: -25px 323 | } 324 | 325 | .box-links-for-menu .list-item li label { 326 | max-width: 80%; 327 | overflow: hidden; 328 | text-overflow: ellipsis; 329 | white-space: nowrap 330 | } 331 | 332 | .box-links-for-menu .list-item li a { 333 | display: inline-block; 334 | overflow: hidden; 335 | padding-left: 20px; 336 | text-overflow: ellipsis; 337 | -ms-text-overflow: ellipsis; 338 | white-space: nowrap; 339 | width: 100% 340 | } 341 | -------------------------------------------------------------------------------- /public/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * load loading 3 | */ 4 | $(document).ajaxStart(function () { 5 | $("#ajax_loader").show(); 6 | }).ajaxStop(function () { 7 | $("#ajax_loader").hide('slow'); 8 | }); 9 | /** 10 | * change label 11 | */ 12 | $(document).on('keyup', '.edit-menu-item-title', function () { 13 | var title = $(this).val(); 14 | var index = $('.edit-menu-item-title').index($(this)); 15 | $('.menu-item-title').eq(index).html(title); 16 | }); 17 | /** 18 | * change url 19 | */ 20 | $(document).on('keyup', '.edit-menu-item-url', function () { 21 | var url = $(this).val(); 22 | var index = $('.edit-menu-item-url').index($(this)); 23 | /** 24 | * limit string 25 | */ 26 | var result = url.slice(0, 30) + (url.length > 30 ? "..." : ""); 27 | $('.menu-item-link').eq(index).html(result); 28 | }); 29 | /** 30 | * add item menu 31 | * type : default or custom 32 | */ 33 | function addItemMenu(e, type) { 34 | let data = []; 35 | let form = $(e).parents('form'); 36 | if (type == "default") { 37 | if (!form.find('input[name="label"]').val() || !form.find('input[name="url"]').val()) { 38 | alert('Please enter label or url'); 39 | return; 40 | } 41 | data.push({ 42 | label: form.find('input[name="label"]').val(), 43 | url: form.find('input[name="url"]').val(), 44 | role: form.find('select[name="role"]').val(), 45 | icon: form.find('input[name="icon"]').val(), 46 | id: $('#idmenu').val() 47 | }); 48 | } else { 49 | let checkbox = form.find('input[name="menu_id"]:checked'); 50 | let flag = false; 51 | for (let index = 0; index < checkbox.length; index++) { 52 | let element = $(checkbox[index]); 53 | data.push({ 54 | label: element.attr('data-label'), 55 | url: element.attr('data-url'), 56 | role: form.find('select[name="role"]').val(), 57 | icon: element.attr('data-icon'), 58 | id: $('#idmenu').val() 59 | }); 60 | if (!element.attr('data-label') || !element.attr('data-url')) { 61 | flag = true; 62 | } 63 | } 64 | if (flag) { 65 | alert('Please enter label or url'); 66 | return; 67 | } 68 | } 69 | $.ajax({ 70 | data: { 71 | data: data 72 | }, 73 | url: URL_CREATE_ITEM_MENU, 74 | type: 'POST', 75 | success: function (response) { 76 | window.location.reload(); 77 | }, 78 | complete: function () { } 79 | }); 80 | } 81 | 82 | function updateItem(id = 0) { 83 | if (id) { 84 | var label = $('#label-menu-' + id).val(); 85 | var clases = $('#clases-menu-' + id).val(); 86 | var url = $('#url-menu-' + id).val(); 87 | var icon = $('#icon-menu-' + id).val(); 88 | var target = $('#target-menu-' + id).val(); 89 | var role_id = 0; 90 | if ($('#role_menu_' + id).length) { 91 | role_id = $('#role_menu_' + id).val(); 92 | } 93 | if (!label || !url) { 94 | alert('Please enter label or url'); 95 | return; 96 | } 97 | var data = { 98 | label: label, 99 | clases: clases, 100 | url: url, 101 | icon: icon, 102 | target: target, 103 | role_id: role_id, 104 | id: id 105 | }; 106 | } else { 107 | var arr_data = []; 108 | let flag = false; 109 | $('.menu-item-settings').each(function (k, v) { 110 | var id = $(this) 111 | .find('.edit-menu-item-id') 112 | .val(); 113 | var label = $(this) 114 | .find('.edit-menu-item-title') 115 | .val(); 116 | var clases = $(this) 117 | .find('.edit-menu-item-classes') 118 | .val(); 119 | var url = $(this) 120 | .find('.edit-menu-item-url') 121 | .val(); 122 | var icon = $(this) 123 | .find('.edit-menu-item-icon') 124 | .val(); 125 | var role = $(this) 126 | .find('.edit-menu-item-role') 127 | .val(); 128 | var target = $(this) 129 | .find('select.edit-menu-item-target option:selected') 130 | .val(); 131 | if (!label || !url) { 132 | flag = true; 133 | } 134 | arr_data.push({ 135 | id: id, 136 | label: label, 137 | class: clases, 138 | link: url, 139 | icon: icon, 140 | target: target, 141 | role_id: role 142 | }); 143 | }); 144 | if (flag) { 145 | alert('Please enter label or url'); 146 | return; 147 | } 148 | var data = { 149 | dataItem: arr_data 150 | }; 151 | } 152 | $.ajax({ 153 | data: data, 154 | url: URL_UPDATE_ITEM_MENU, 155 | type: 'POST', 156 | beforeSend: function (xhr) { 157 | if (id) { } 158 | }, 159 | success: function (response) { }, 160 | complete: function () { 161 | if (id) { } 162 | } 163 | }); 164 | } 165 | 166 | function actualizarMenu(serialize) { 167 | if ($('#menu-name').val()) { 168 | $.ajax({ 169 | dataType: 'json', 170 | data: { 171 | data: serialize, 172 | menuName: $('#menu-name').val(), 173 | idMenu: $('#idmenu').val(), 174 | class: $('#menu-class').val() 175 | }, 176 | url: URL_UPDATE_ITEMS_AND_MENU, 177 | type: 'POST', 178 | success: function (response) { 179 | /** 180 | * update text option 181 | */ 182 | $(`select[name="menu"] option[value="${$('#idmenu').val()}"]`).html($('#menu-name').val()); 183 | } 184 | }); 185 | }else{ 186 | alert('Please enter name menu!'); 187 | } 188 | } 189 | 190 | function deleteItem(id) { 191 | $.ajax({ 192 | dataType: 'json', 193 | data: { 194 | id: id 195 | }, 196 | url: URL_DELETE_ITEM_MENU, 197 | type: 'POST', 198 | success: function (response) { 199 | window.location = URL_FULL; 200 | } 201 | }); 202 | } 203 | 204 | function deleteMenu() { 205 | var r = confirm('Do you want to delete this menu ?'); 206 | if (r == true) { 207 | $.ajax({ 208 | dataType: 'json', 209 | data: { 210 | id: $('#idmenu').val() 211 | }, 212 | url: URL_DELETE_MENU, 213 | type: 'POST', 214 | success: function (response) { 215 | if (!response.error) { 216 | alert(response.resp); 217 | window.location = URL_CURRENT; 218 | } else { 219 | alert(response.resp); 220 | } 221 | } 222 | }); 223 | } else { 224 | return false; 225 | } 226 | } 227 | 228 | function createNewMenu() { 229 | if (!!$('#menu-name').val()) { 230 | $.ajax({ 231 | dataType: 'json', 232 | data: { 233 | name: $('#menu-name').val(), 234 | class: $('#menu-class').val(), 235 | }, 236 | url: URL_CREATE_MENU, 237 | type: 'POST', 238 | success: function (response) { 239 | window.location = URL_CURRENT + '?menu=' + response.resp; 240 | } 241 | }); 242 | } else { 243 | alert('Please enter name menu'); 244 | $('#menu-name').focus(); 245 | return false; 246 | } 247 | } 248 | 249 | 250 | $(document).ready(function () { 251 | if ($('#nestable').length) { 252 | /** 253 | * https://github.com/RamonSmit/Nestable2#configuration 254 | */ 255 | $('#nestable').nestable({ 256 | expandBtnHTML: '', 257 | collapseBtnHTML: '', 258 | maxDepth: 5, //number of levels an item can be nested 259 | callback: function (l, e) { 260 | updateItem(); 261 | actualizarMenu(l.nestable('toArray')); 262 | } 263 | }); 264 | } 265 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Drag and Drop menu 2 | [![Latest Stable Version](https://poser.pugx.org/nguyendachuy/laravel-menu/v)](//packagist.org/packages/nguyendachuy/laravel-menu) [![Total Downloads](https://poser.pugx.org/nguyendachuy/laravel-menu/downloads)](//packagist.org/packages/nguyendachuy/laravel-menu) [![Latest Unstable Version](https://poser.pugx.org/nguyendachuy/laravel-menu/v/unstable)](//packagist.org/packages/nguyendachuy/laravel-menu) [![License](https://poser.pugx.org/nguyendachuy/laravel-menu/license)](//packagist.org/packages/nguyendachuy/laravel-menu) 3 | 4 | 5 | ![Laravel drag and drop menu](https://raw.githubusercontent.com/nguyendachuy/laravel-menu/master/screenshot.png) 6 | 7 | ## Important Notice: Major Upgrade Available 8 | 9 | We're excited to announce a major upgrade of Laravel Drag and Drop Menu Builder available on the `upgrade-version` branch. This version adds significant new features and improvements, especially comprehensive multilingual support. 10 | 11 | ### New Features and Improvements 12 | 13 | - **Complete Multilingual Support**: User interface and system messages translated into multiple languages (currently English and Vietnamese) 14 | - **Migration from Bootstrap to Tailwind CSS**: Completely redesigned interface with Tailwind CSS, providing a more modern and flexible experience 15 | - **Mega Menu Support**: Create mega menus with custom content 16 | - **Advanced JavaScript Integration**: Translation system integrated into JavaScript, allowing messages to be displayed in the user's language 17 | - **Improved UX/UI**: Modern interface with intuitive drag-and-drop feedback 18 | - **Performance Optimization**: Improved performance with caching system and query optimization 19 | - **Robust Error Handling**: Comprehensive validation and error recovery system 20 | - **Critical Bug Fixes**: Fixed issues including Menu Name and Menu Class not updating when clicking Update Menu 21 | 22 | ### How to Use the `upgrade-version` Branch 23 | 24 | #### Option 1: Direct Git Checkout 25 | 26 | ```bash 27 | # Clone repository (if you don't have it) 28 | git clone https://github.com/nguyendachuy/laravel-menu.git 29 | 30 | # Switch to upgrade-version branch 31 | git checkout upgrade-version 32 | 33 | # Install dependencies 34 | composer install 35 | ``` 36 | 37 | #### Option 2: Using Composer (Recommended for Projects) 38 | 39 | Update your `composer.json` file to use the upgrade-version branch: 40 | 41 | ```json 42 | { 43 | "require": { 44 | "nguyendachuy/laravel-menu": "dev-upgrade-version" 45 | } 46 | } 47 | ``` 48 | 49 | Then run: 50 | 51 | ```bash 52 | composer update nguyendachuy/laravel-menu 53 | ``` 54 | 55 | ### Database Updates 56 | 57 | The new version adds columns for the Mega Menu feature. Run migrations to update your database structure: 58 | 59 | ```bash 60 | php artisan migrate 61 | ``` 62 | 63 | The migration will add the following columns to the `menu_items` table: 64 | - `is_mega_menu` (boolean): Determines if an item is a mega menu 65 | - `mega_menu_content` (text): Stores the HTML content of the mega menu 66 | 67 | ### Updating Resources 68 | 69 | The new version includes many changes to config, views, and public files. Run the following command to update: 70 | 71 | ```bash 72 | # Publish all resources 73 | php artisan vendor:publish --provider="NguyenHuy\Menu\Providers\MenuServiceProvider" --force 74 | 75 | # Or publish specific resource types 76 | php artisan vendor:publish --tag=laravel-menu-config --force 77 | php artisan vendor:publish --tag=laravel-menu-views --force 78 | php artisan vendor:publish --tag=laravel-menu-assets --force 79 | php artisan vendor:publish --tag=laravel-menu-translations --force 80 | ``` 81 | 82 | **Important Note**: Using `--force` will overwrite any customizations you've made. If you've customized files, back them up before running these commands. 83 | 84 | ### Important Note 85 | 86 | The `upgrade-version` branch has not yet been merged into `master` to avoid affecting current users. We encourage you to test this branch in a development environment before applying it to production projects. 87 | 88 | Feedback and contributions are welcome to help us improve this package before the official release. 89 | 90 | Please refer to the README.md in the `upgrade-version` branch for complete details on new features and usage instructions. 91 | 92 | ### Installation 93 | 94 | 1. Run 95 | 96 | ```php 97 | composer require nguyendachuy/laravel-menu 98 | ``` 99 | 2. Run publish 100 | 101 | ```php 102 | php artisan vendor:publish --provider="NguyenHuy\Menu\Providers\MenuServiceProvider" 103 | ``` 104 | 105 | 3. Configure (optional) in **_config/menu.php_** : 106 | 107 | - **_CUSTOM MIDDLEWARE:_** You can add you own middleware 108 | - **_TABLE PREFIX:_** By default this package will create 2 new tables named "menus" and "menu_items" but you can still add your own table prefix avoiding conflict with existing table 109 | - **_TABLE NAMES_** If you want use specific name of tables you have to modify that and the migrations 110 | - **_Custom routes_** If you want to edit the route path you can edit the field 111 | - **_Role Access_** If you want to enable roles (permissions) on menu items 112 | - **_CACHE ENABLED:_** Set this to `true` if you want to enable caching for menu items. Default is `false`. 113 | - **_CACHE KEY PREFIX:_** The prefix to use for cache keys. Default is `'menu'`. 114 | - **_CACHE TTL:_** The time-to-live (in minutes) for cached menu items. Default is `60`. 115 | 116 | 4. Run migrate 117 | 118 | ```php 119 | php artisan migrate 120 | ``` 121 | 122 | DONE 123 | 124 | ### Menu Builder Usage Example - displays the builder 125 | 126 | On your view blade file 127 | 128 | ```php 129 | @extends('app') 130 | 131 | @section('contents') 132 | {!! Menu::render() !!} 133 | @endsection 134 | 135 | //YOU MUST HAVE JQUERY LOADED BEFORE menu scripts 136 | @push('scripts') 137 | {!! Menu::scripts() !!} 138 | @endpush 139 | ``` 140 | 141 | ### Using The Model 142 | 143 | Call the model class 144 | 145 | ```php 146 | use NguyenHuy\Menu\Models\Menus; 147 | use NguyenHuy\Menu\Models\MenuItems; 148 | 149 | ``` 150 | 151 | ### Menu Usage Example (a) 152 | 153 | A basic two-level menu can be displayed in your blade template 154 | 155 | ##### Using Model Class 156 | ```php 157 | 158 | /* get menu by id*/ 159 | 160 | $menu = Menus::find(1); 161 | /* or by name */ 162 | $menu = Menus::where('name','Test Menu')->first(); 163 | 164 | /* or get menu by name and the items with EAGER LOADING (RECOMENDED for better performance and less query call)*/ 165 | $menu = Menus::where('name','Test Menu')->with('items')->first(); 166 | /*or by id */ 167 | $menu = Menus::where('id', 1)->with('items')->first(); 168 | 169 | //you can access by model result 170 | $public_menu = $menu->items; 171 | 172 | //or you can convert it to array 173 | $public_menu = $menu->items->toArray(); 174 | 175 | ``` 176 | 177 | ##### or Using helper 178 | ```php 179 | // Using Helper 180 | $public_menu = Menu::getByName('Public'); //return array 181 | 182 | ``` 183 | 184 | ### Menu Usage Example (b) 185 | 186 | Now inside your blade template file place the menu using this simple example 187 | 188 | ```php 189 | 214 | ``` 215 | 216 | ### HELPERS 217 | 218 | ### Get Menu Items By Menu ID 219 | 220 | ```php 221 | use NguyenHuy\Menu\Facades\Menu; 222 | ... 223 | /* 224 | Parameter: Menu ID 225 | Return: Array 226 | */ 227 | $menuList = Menu::get(1); 228 | ``` 229 | 230 | ### Get Menu Items By Menu Name 231 | 232 | In this example, you must have a menu named _Admin_ 233 | 234 | ```php 235 | use NguyenHuy\Menu\Facades\Menu; 236 | ... 237 | /* 238 | Parameter: Menu ID 239 | Return: Array 240 | */ 241 | $menuList = Menu::getByName('Admin'); 242 | ``` 243 | 244 | ### Customization 245 | 246 | You can edit the menu interface in **_resources/views/vendor/nguyendachuy-menu/menu-html.blade.php_** 247 | -------------------------------------------------------------------------------- /src/Database/Query/CacheableQueryBuilder.php: -------------------------------------------------------------------------------- 1 | modelClass = $modelClass ?? static::class; 49 | 50 | // Load configuration values from the menu.cache configuration file 51 | $this->minutes = config('menu.cache.minutes', $this->minutes); 52 | $this->enabled = config('menu.cache.enabled', $this->enabled); 53 | $this->prefix = config('menu.cache.prefix', $this->prefix); 54 | } 55 | 56 | 57 | /** 58 | * Pass our configuration to newly created queries 59 | * 60 | * @return $this|CacheableQueryBuilder 61 | */ 62 | public function newQuery() 63 | { 64 | return new static( 65 | $this->connection, 66 | $this->grammar, 67 | $this->processor, 68 | $this->modelClass 69 | ); 70 | } 71 | 72 | 73 | /** 74 | * Run the query as a "select" statement against the connection. 75 | * 76 | * Check the cache based on the query beforehand and return 77 | * a cached value or cache it if not already. 78 | * 79 | * @return array 80 | */ 81 | protected function runSelect() 82 | { 83 | if (!$this->enabled) { 84 | return parent::runSelect(); 85 | } 86 | 87 | // Use the query as the cache key 88 | $cacheKey = $this->getCacheKey(); 89 | 90 | // Check if the cache store supports tags 91 | $isTaggableStore = Cache::getStore() instanceof TaggableStore; 92 | // Create additional identifiers based on the model class 93 | $modelClasses = $this->getIdentifiableModelClasses($this->getIdentifiableValue()); 94 | 95 | // If the query is already cached, return the cached value 96 | if (($isTaggableStore && Cache::tags($modelClasses)->has($cacheKey)) || Cache::has($cacheKey)) { 97 | return $isTaggableStore ? Cache::tags($modelClasses)->get($cacheKey) : Cache::get($cacheKey); 98 | } 99 | 100 | // If not cached, run the query and cache the result 101 | $retVal = parent::runSelect(); 102 | 103 | // If the cache store supports tags, cache the result with tags 104 | if ($isTaggableStore) { 105 | Cache::tags($modelClasses)->put($cacheKey, $retVal, $this->minutes); 106 | } else { 107 | // If not, cache the result and store the query for purging purposes 108 | foreach ($modelClasses as $modelClass) { 109 | $modelCacheKey = $this->getModelCacheKey($modelClass); 110 | $queries = Cache::get($modelCacheKey, []); 111 | $queries[] = $cacheKey; 112 | Cache::put($modelCacheKey, $queries); 113 | } 114 | Cache::put($cacheKey, $retVal, $this->minutes); 115 | } 116 | 117 | return $retVal; 118 | } 119 | 120 | /** 121 | * Check if to cache against just the class or a specific identifiable e.g. id 122 | * 123 | * @return string[] 124 | */ 125 | protected function getIdentifiableModelClasses($value = null): array 126 | { 127 | $retVals = [$this->modelClass]; 128 | if ($value) { 129 | if (is_array($value)) { 130 | foreach ($value as $v) { 131 | $retVals[] = "{$this->modelClass}#{$v}"; 132 | } 133 | } else { 134 | $retVals[] = "{$this->modelClass}#{$value}"; 135 | } 136 | } 137 | 138 | return $retVals; 139 | } 140 | 141 | /** 142 | * Get the identifiable value from the query's where clauses 143 | * 144 | * @param array|null $wheres 145 | * @return mixed|null 146 | */ 147 | protected function getIdentifiableValue(array $wheres = null) 148 | { 149 | $wheres = $wheres ?? $this->wheres; 150 | foreach ($wheres as $where) { 151 | if (isset($where['type']) && $where['type'] === 'Nested') { 152 | return $this->getIdentifiableValue($where['query']->wheres); 153 | } 154 | if (isset($where['column']) && $where['column'] === 'id') { 155 | return $where['value'] ?? $where['values']; 156 | } 157 | } 158 | 159 | return null; 160 | } 161 | 162 | /** 163 | * Check if the query is an identifier query (id-driven) 164 | * 165 | * @param array|null $wheres 166 | * @return bool 167 | */ 168 | protected function isIdentifiableQuery(array $wheres = null): bool 169 | { 170 | return $this->getIdentifiableValue($wheres) !== null; 171 | } 172 | 173 | /** 174 | * Purge all model queries and results from the cache 175 | * 176 | * @param null $identifier 177 | * @return bool 178 | */ 179 | public function flushCache($identifier = null): bool 180 | { 181 | if (!$this->enabled) { 182 | return false; 183 | } 184 | // If the cache store supports tags, flush all results with the specified model classes 185 | $modelClasses = $this->getIdentifiableModelClasses($identifier); 186 | if (Cache::getStore() instanceof TaggableStore) { 187 | return Cache::tags($modelClasses)->flush(); 188 | } else { 189 | // If not, forget the cached queries and results based on the model classes 190 | foreach ($modelClasses as $modelClass) { 191 | $modelCacheKey = $this->getModelCacheKey($modelClass); 192 | $queries = Cache::get($modelCacheKey); 193 | if (!empty($queries)) { 194 | foreach ($queries as $query) { 195 | Cache::forget($query); 196 | } 197 | 198 | Cache::forget($modelCacheKey); 199 | } 200 | } 201 | } 202 | 203 | return true; 204 | } 205 | 206 | /** 207 | * Build a cache key based on the SQL statement and its bindings 208 | * 209 | * @return string 210 | */ 211 | protected function getCacheKey(): string 212 | { 213 | $sql = $this->toSql(); 214 | $bindings = $this->getBindings(); 215 | if (!empty($bindings)) { 216 | $bindings = implode('_', $this->getBindings()); 217 | 218 | return $sql . '_' . $bindings; 219 | } 220 | 221 | return $sql; 222 | } 223 | 224 | /** 225 | * Get the cache key for the specified model class 226 | * 227 | * @param string|null $modelClass 228 | * @return string 229 | */ 230 | protected function getModelCacheKey(string $modelClass = null): string 231 | { 232 | return $this->prefix . '_' . ($modelClass ?? $this->modelClass); 233 | } 234 | 235 | /** 236 | * Update records in the database and flush the cache 237 | * 238 | * @param array $values 239 | * @return int 240 | */ 241 | public function update(array $values) 242 | { 243 | $this->flushCache(); 244 | 245 | return parent::update($values); 246 | } 247 | 248 | /** 249 | * Update records in the database from another query and flush the cache 250 | * 251 | * @param array $values 252 | * @return int 253 | */ 254 | public function updateFrom(array $values) 255 | { 256 | $this->flushCache(); 257 | 258 | return parent::updateFrom($values); 259 | } 260 | 261 | /** 262 | * Insert records into the database and flush the cache 263 | * 264 | * @param array $values 265 | * @return bool 266 | */ 267 | public function insert(array $values) 268 | { 269 | $this->flushCache(); 270 | 271 | return parent::insert($values); 272 | } 273 | 274 | /** 275 | * Insert records into the database and return the last inserted ID, flushing the cache 276 | * 277 | * @param array $values 278 | * @param $sequence 279 | * @return int 280 | */ 281 | public function insertGetId(array $values, $sequence = null) 282 | { 283 | $this->flushCache(); 284 | 285 | return parent::insertGetId($values, $sequence); 286 | } 287 | 288 | /** 289 | * Insert records into the database, ignoring duplicates, and flush the cache 290 | * 291 | * @param array $values 292 | * @return int 293 | */ 294 | public function insertOrIgnore(array $values) 295 | { 296 | $this->flushCache(); 297 | 298 | return parent::insertOrIgnore($values); 299 | } 300 | 301 | /** 302 | * Insert records into the database using a subquery and flush the cache 303 | * 304 | * @param array $columns 305 | * @param $query 306 | * @return int 307 | */ 308 | public function insertUsing(array $columns, $query) 309 | { 310 | $this->flushCache(); 311 | 312 | return parent::insertUsing($columns, $query); 313 | } 314 | 315 | /** 316 | * Upsert records in the database and flush the cache 317 | * 318 | * @param array $values 319 | * @param $uniqueBy 320 | * @param $update 321 | * @return int 322 | */ 323 | public function upsert(array $values, $uniqueBy, $update = null) 324 | { 325 | $this->flushCache(); 326 | 327 | return parent::upsert($values, $uniqueBy, $update); 328 | } 329 | 330 | /** 331 | * Delete records from the database and flush the cache 332 | * 333 | * @param $id 334 | * @return int 335 | */ 336 | public function delete($id = null) 337 | { 338 | $this->flushCache($id); 339 | 340 | return parent::delete($id); 341 | } 342 | 343 | /** 344 | * Truncate the table and flush the cache 345 | * 346 | * @return void 347 | */ 348 | public function truncate() 349 | { 350 | $this->flushCache(); 351 | 352 | parent::truncate(); 353 | } 354 | } 355 | --------------------------------------------------------------------------------