|
27 | 27 | <div id="accordion" role="tablist" aria-multiselectable="true"> |
28 | 28 | @forelse($logs as $key => $log) |
29 | 29 | <div class="card mb-0 pb-0"> |
30 | | - <div class="card-header bg-{{ $log['level_class'] }}" role="tab" id="heading{{ $key }}"> |
31 | | - <a role="button" data-toggle="collapse" data-bs-toggle="collapse" data-parent="#accordion" href="#collapse{{ $key }}" aria-expanded="true" aria-controls="collapse{{ $key }}" class="text-white"> |
| 30 | + <div class="card-header bg-{{ $log['level_class'] }} d-flex align-items-center" role="tab" id="heading{{ $key }}"> |
| 31 | + <a href="#heading{{ $key }}" class="log-anchor text-white me-2 mr-2 border border-white rounded-circle text-center text-decoration-none flex-shrink-0" style="width: 30px; height: 30px; line-height: 28px;"> |
| 32 | + <i class="la la-link"></i> |
| 33 | + </a> |
| 34 | + <a role="button" data-toggle="collapse" data-bs-toggle="collapse" data-parent="#accordion" href="#collapse{{ $key }}" aria-expanded="false" aria-controls="collapse{{ $key }}" class="text-white flex-grow-1 collapsed"> |
32 | 35 | <i class="la la-{{ $log['level_img'] }}"></i> |
33 | 36 | <span>[{{ $log['date'] }}]</span> |
34 | 37 | {{ Str::limit($log['text'], 150) }} |
35 | 38 | </a> |
36 | 39 | </div> |
37 | | - <div id="collapse{{ $key }}" class="panel-collapse collapse p-3" role="tabpanel" aria-labelledby="heading{{ $key }}"> |
| 40 | + <div id="collapse{{ $key }}" class="panel-collapse collapse p-3" role="tabpanel" aria-labelledby="heading{{ $key }}" data-parent="#accordion" data-bs-parent="#accordion"> |
38 | 41 | <div class="panel-body"> |
39 | 42 | <p>{{$log['text']}}</p> |
40 | | - <pre class="p-0" ><code class="php">{{ trim($log['stack']) }}</code></pre> |
| 43 | + <pre class="p-0" ><code>{{ trim($log['stack']) }}</code></pre> |
41 | 44 | </div> |
42 | 45 | </div> |
43 | 46 | </div> |
|
49 | 52 | @endsection |
50 | 53 |
|
51 | 54 | @section('after_scripts') |
52 | | - <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/default.min.css"> |
53 | | - <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/highlight.min.js"></script> |
54 | | - <script>hljs.initHighlightingOnLoad();</script> |
| 55 | + <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css"> |
| 56 | + <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> |
| 57 | + <script>hljs.highlightAll();</script> |
| 58 | + <script> |
| 59 | + function openAccordionFromHash() { |
| 60 | + if (window.location.hash && window.location.hash.startsWith('#heading')) { |
| 61 | + const heading = document.querySelector(window.location.hash); |
| 62 | + if (heading) { |
| 63 | + const trigger = heading.querySelector('a[data-toggle="collapse"], a[data-bs-toggle="collapse"]'); |
| 64 | +
|
| 65 | + if (trigger) { |
| 66 | + const targetId = trigger.getAttribute('href') || trigger.getAttribute('data-bs-target'); |
| 67 | + const collapseEl = document.querySelector(targetId); |
| 68 | + const isExpanded = collapseEl && (collapseEl.classList.contains('show') || collapseEl.classList.contains('in')); |
| 69 | +
|
| 70 | + if (!isExpanded) { |
| 71 | + trigger.click(); |
| 72 | + |
| 73 | + try { |
| 74 | + if (collapseEl && !collapseEl.classList.contains('show')) { |
| 75 | + const bsCollapse = bootstrap.Collapse.getInstance(collapseEl) || new bootstrap.Collapse(collapseEl, { toggle: false }); |
| 76 | + bsCollapse.show(); |
| 77 | + } |
| 78 | + } catch (e) { |
| 79 | + // |
| 80 | + } |
| 81 | + } |
| 82 | + } |
| 83 | +
|
| 84 | + setTimeout(() => { |
| 85 | + const elementPosition = heading.getBoundingClientRect().top + window.scrollY; |
| 86 | + const offsetPosition = elementPosition - 80; |
| 87 | + |
| 88 | + window.scrollTo({ |
| 89 | + top: offsetPosition, |
| 90 | + behavior: "smooth" |
| 91 | + }); |
| 92 | + }, 250); |
| 93 | + } |
| 94 | + } |
| 95 | + } |
| 96 | +
|
| 97 | + document.addEventListener('DOMContentLoaded', () => { |
| 98 | + openAccordionFromHash(); |
| 99 | +
|
| 100 | + window.addEventListener('hashchange', () => { |
| 101 | + openAccordionFromHash(); |
| 102 | + }); |
| 103 | +
|
| 104 | +
|
| 105 | + function showNotification(type, text) { |
| 106 | + new Noty({ |
| 107 | + text: text, |
| 108 | + type: type |
| 109 | + }).show(); |
| 110 | + } |
| 111 | +
|
| 112 | + async function copyToClipboard(text) { |
| 113 | + try { |
| 114 | + if (navigator.clipboard) { |
| 115 | + await navigator.clipboard.writeText(text); |
| 116 | + showNotification('success', "{{ trans('backpack::logmanager.link_copied_to_clipboard') }}"); |
| 117 | + } else { |
| 118 | + throw new Error('Clipboard API unavailable'); |
| 119 | + } |
| 120 | + } catch (err) { |
| 121 | + // Fallback |
| 122 | + const textArea = document.createElement("textarea"); |
| 123 | + textArea.value = text; |
| 124 | + document.body.appendChild(textArea); |
| 125 | + textArea.select(); |
| 126 | + try { |
| 127 | + document.execCommand('copy'); |
| 128 | + showNotification('success', "{{ trans('backpack::logmanager.link_copied_to_clipboard') }}"); |
| 129 | + } catch (fallbackErr) { |
| 130 | + showNotification('error', "{{ trans('backpack::logmanager.link_copy_failed') }}"); |
| 131 | + } |
| 132 | + document.body.removeChild(textArea); |
| 133 | + } |
| 134 | + } |
| 135 | +
|
| 136 | + document.querySelectorAll('.log-anchor').forEach(anchor => { |
| 137 | + anchor.addEventListener('click', function(e) { |
| 138 | + const url = window.location.origin + window.location.pathname + this.getAttribute('href'); |
| 139 | + copyToClipboard(url); |
| 140 | + }); |
| 141 | + }); |
| 142 | + }); |
| 143 | + </script> |
55 | 144 | @endsection |
0 commit comments