1<?php2 3use App\Livewire\ThirdParty;4 5Route::get('/third-party', ThirdParty::class);
1<?php 2 3namespace App\Livewire; 4 5use Livewire\Attributes\On; 6use Livewire\Component; 7 8class ThirdParty extends Component 9{10 public $options = [];11 public $selectedOption = '1';12 public $selectedOptions = ['3', '4'];13 public $visible = true;14 public $text;15 public $sortableData;16 public $sortableItem;17 18 public function mount()19 {20 $this->options = [21 [22 'id' => '1',23 'name' => 'Jujutsu Kaisen'24 ],25 [26 'id' => '2',27 'name' => 'One Piece'28 ],29 [30 'id' => '3',31 'name' => 'Elusive Samurai'32 ],33 [34 'id' => '4',35 'name' => 'Black Clover'36 ]37 ];38 }39 40 function clearSelectedOption()41 {42 $this->selectedOption = '';43 }44 45 function clearSelectedOptions()46 {47 $this->selectedOptions = [];48 }49 50 #[On('destroy')]51 function destroy()52 {53 // Mock delete process in server with sleep function54 sleep(3);55 $this->dispatch('alert:ok', data: [56 'message' => 'Item has been deleted!'57 ]);58 $this->visible = false;59 }60 61 public function submitEditor()62 {63 // May be add some validation here!64 }65 66 #[On('set-data')]67 function setSortableData($data)68 {69 $this->sortableData = $data;70 }71 72 public function submitSortable()73 {74 if (!empty($this->sortableItem)) {75 $this->dispatch('nested-sortable-store-item', data: [76 'item' => $this->sortableItem77 ]);78 $this->sortableItem = '';79 }80 }81 82 public function render()83 {84 return view('livewire.third-party')85 ->title('Third Party');86 }87}
1@push('styles') 2<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet"> 3<style> 4 [x-cloak] { 5 display: none !important; 6 } 7 8 /* Nested sortable style */ 9 ul { 10 list-style-type: none; 11 margin: 0; 12 } 13 14 .list-group-item { 15 border: 1px solid rgba(0, 0, 0, .125); 16 padding: .5rem; 17 cursor: move; 18 } 19 20 .list-group-item:last-child { 21 border-bottom-left-radius: .25rem; 22 border-bottom-right-radius: .25rem; 23 } 24 25 .list-group-item:hover { 26 z-index: 0; 27 } 28 29 .nested-sortable, 30 .nested-1, 31 .nested-2, 32 .nested-3 { 33 margin-top: 5px; 34 } 35 36 .nested-1 { 37 background-color: #e6e6e6; 38 } 39 40 .nested-2 { 41 background-color: #cccccc; 42 } 43 44 .nested-3 { 45 background-color: #b3b3b3; 46 } 47</style> 48<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trix/dist/trix.min.css" crossorigin="anonymous"> 49@endpush 50@push('scripts') 51<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script> 52<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js" crossorigin="anonymous"></script> 53<script> 54 function confirmDestroy(el) { 55 swal({ 56 title: 'Warning!', 57 text: el.getAttribute('data-message'), 58 icon: 'warning', 59 buttons: true, 60 dangerMode: true, 61 }) 62 .then((willDelete) => { 63 if (willDelete) { 64 swal('Data is being deleted. Please wait...', { 65 icon: 'info', 66 closeOnClickOutside: false, 67 button: false 68 }); 69 window.Livewire.dispatch('destroy'); 70 } else { 71 swal('Your data still saved!') 72 } 73 }) 74 } 75 76 window.addEventListener('alert:ok', function (e) { 77 swal({ 78 icon: 'success', 79 text: e.detail.data.message 80 }); 81 }) 82</script> 83<script src="https://cdn.jsdelivr.net/npm/trix/dist/trix.umd.min.js" crossorigin="anonymous"></script> 84<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script> 85<script> 86 let nestedSortables = Array.from(document.querySelectorAll('.nested-sortable')); 87 for (let i = 0; i < nestedSortables.length; i++) { 88 new Sortable(nestedSortables[i], { 89 group: 'nested', 90 animation: 150, 91 fallbackOnBody: true, 92 swapThreshold: 0.65, 93 onEnd: function (evt) { 94 let newData = JSON.stringify(serialize(root)); 95 console.log(newData); 96 Livewire.dispatch('set-data', { data: newData }); 97 } 98 }); 99 }100 101 const nestedQuery = '.nested-sortable';102 const identifier = 'id';103 const root = document.getElementById('wrapper-nested-sortable');104 function serialize(sortable) {105 const serialized = [];106 const children = [].slice.call(sortable.children);107 for (const i in children) {108 const nested = children[i].querySelector(nestedQuery);109 const parentId = children[i].closest(nestedQuery).getAttribute('data-parent-id');110 serialized.push({111 id: children[i].dataset[identifier],112 parent_id: parentId,113 children: nested ? serialize(nested) : [],114 order: (parseInt(i) + 1).toString(),115 });116 }117 return serialized;118 }119 120 window.addEventListener('nested-sortable-store-item', (e) => {121 document.getElementById('wrapper-nested-sortable').appendChild(122 document123 .createRange()124 .createContextualFragment(`<li data-id="${e.detail.data.item}" data-parent="0"125 class="list-group-item nested-1">${e.detail.data.item}126 </li>`));127 });128</script>129@endpush130 131<div>132 <a href="/" class="underline text-blue-500">Back</a>133 <h1 class="text-2xl mb-4">Third Party</h1>134 135 <h2 class="text-xl mb-4">Select with Tom Select</h2>136 <h3 class="text-lg">Single option</h3>137 <div wire:ignore>138 <select x-data="{139 tomSelectInstance: null,140 options: {{ collect($options) }},141 items: $wire.entangle('selectedOption').live142 }"143 x-init="tomSelectInstance = new TomSelect($refs.select, {144 valueField: 'id',145 labelField: 'name',146 searchField: 'name',147 options: options,148 items: items,149 onItemAdd: function () {150 this.setTextboxValue('');151 }152 }); $watch('items', (value, oldValue) => {153 const result = JSON.parse(JSON.stringify(value));154 if (result.length === 0) {155 tomSelectInstance.clear();156 }157 });"158 x-ref="select"159 x-cloak160 id="selectedOption"161 wire:model.live="selectedOption"162 autocomplete="off">163 164 </select>165 </div>166 <p class="mb-4">Selected option: {{ @json_encode($selectedOption) }}</p>167 168 <h3 class="text-lg">Multiple options</h3>169 <div wire:ignore>170 <select x-data="{171 tomSelectInstance: null,172 options: {{ collect($options) }},173 items: $wire.entangle('selectedOptions').live174 }"175 x-init="tomSelectInstance = new TomSelect($refs.select, {176 valueField: 'id',177 labelField: 'name',178 searchField: 'name',179 options: options,180 items: items,181 maxItems: 2,182 onItemAdd: function () {183 this.setTextboxValue('');184 this.refreshOptions();185 }186 }); $watch('items', (value, oldValue) => {187 const result = JSON.parse(JSON.stringify(value));188 if (result.length === 0) {189 tomSelectInstance.clear();190 }191 })"192 x-ref="select"193 x-cloak194 id="selectedOptions"195 wire:model.live="selectedOptions"196 multiple197 autocomplete="off">198 199 </select>200 </div>201 <p class="mb-4">Selected options: {{ @json_encode($selectedOptions) }}</p>202 203 <h2 class="text-xl mb-4">Alert with Sweet Alert</h2>204 @if ($visible)205 <div class="flex items-center gap-2 mb-4">206 <p>Delete me and I will say good bye!</p>207 <button onclick="confirmDestroy(this);" data-message="Are you sure to delete this item?" class="bg-red-500 p-1 rounded text-white">Delete</button>208 </div>209 @else210 <p class="mb-4">Good bye! 👋🏼</p>211 @endif212 213 <h2 class="text-xl mb-4">Text Area with Trix Editor</h2>214 <form class="mb-4" wire:submit="submitEditor">215 <input type="hidden" id="x">216 <div wire:ignore>217 <trix-editor x-data="{218 setValue(event) {219 $wire.text = event.target.value;220 },221 upload(event) {222 const data = new FormData();223 data.append('file', event.attachment.file);224 225 fetch('/upload-file', {226 headers: {227 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').getAttribute('content')228 },229 method: 'POST',230 credentials: 'same-origin',231 body: data232 })233 .then(response => response.json())234 .then(data => {235 event.attachment.setAttributes({236 url: data.image_url237 });238 });239 }240 }"241 x-on:trix-file-accept="event.preventDefault()"242 x-on:trix-attachment-add="upload"243 x-on:trix-change="setValue"244 x-ref="trix"245 input="x" class="trix-content"></trix-editor>246 </div>247 <button class="bg-blue-500 text-white p-2 rounded my-4">Submit</button>248 {!! $text !!}249 </form>250 251 <h2 class="text-xl mb-4">Nested Sortable</h2>252 <p class="mb-4">Usually, I use nested sortable to display tree menu then drag and drop menu position. Imagine you build dynamic menus for your site.</p>253 <form class="flex gap-2" wire:submit.prevent="submitSortable">254 <input type="text" wire:model="sortableItem" placeholder="Insert an item" class="flex-1 rounded">255 <button type="submit" class="bg-blue-500 p-2 rounded text-white">Submit</button>256 </form>257 <div wire:ignore>258 {!! build_sortable_list(get_menus()) !!}259 </div>260 @if (!empty($data))261 <pre style="overflow-x: scroll;">262 <code>263 {{ @json_encode($data) }}264 </code>265 </pre>266 @else267 <p>Please drag and drop the list above to see updated list.</p>268 @endif269</div>
Selected option: "1"
Selected options: ["3","4"]
Delete me and I will say good bye!
Usually, I use nested sortable to display tree menu then drag and drop menu position. Imagine you build dynamic menus for your site.
Please drag and drop the list above to see updated list.