├── .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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
20 | {{-- @if($key == 0) show @endif --}}
21 |
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 | [](//packagist.org/packages/nguyendachuy/laravel-menu) [](//packagist.org/packages/nguyendachuy/laravel-menu) [](//packagist.org/packages/nguyendachuy/laravel-menu) [](//packagist.org/packages/nguyendachuy/laravel-menu)
3 |
4 |
5 | 
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 |
190 |
193 |
213 |
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 |
--------------------------------------------------------------------------------