src/ApplicationBundle/Resources/views/inc/menu/_spotlight_search.html.twig line 1

Open in your IDE?
  1. {# Self-contained Ctrl+K / ⌘K spotlight search for cp-shell pages (main_layout).
  2.    Mirrors the classic skeleton palette; hits the same `spotlight_search` endpoint
  3.    so results stay consistent. Include once near </body>. #}
  4. <div id="cpSpot" class="cpspot" role="dialog" aria-hidden="true">
  5.     <div class="cpspot-box">
  6.         <div class="cpspot-input">
  7.             <i class="fa fa-search"></i>
  8.             <input type="text" id="cpSpotInput" placeholder="Search modules, clients, suppliers, documents…" autocomplete="off" spellcheck="false">
  9.             <kbd>Esc</kbd>
  10.         </div>
  11.         <div class="cpspot-results" id="cpSpotResults">
  12.             <div class="cpspot-empty">Type to search across modules, records and documents</div>
  13.         </div>
  14.         <div class="cpspot-foot"><span><kbd>↑</kbd><kbd>↓</kbd> navigate</span><span><kbd>↵</kbd> open</span><span><kbd>Ctrl</kbd>+<kbd>K</kbd></span></div>
  15.     </div>
  16. </div>
  17. <style>
  18.     .cpspot{display:none;position:fixed;inset:0;z-index:3000;background:rgba(20,20,40,.45);align-items:flex-start;justify-content:center;}
  19.     .cpspot.open{display:flex;}
  20.     .cpspot-box{margin-top:9vh;width:600px;max-width:92vw;background:#fff;border-radius:14px;box-shadow:0 30px 80px -20px rgba(0,0,0,.5);overflow:hidden;display:flex;flex-direction:column;max-height:70vh;}
  21.     .cpspot-input{display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid #eef0f4;}
  22.     .cpspot-input i{color:#b58a63;}
  23.     .cpspot-input input{flex:1;border:0;outline:none;font-size:var(--cp-fs-lg, 16px);background:transparent;}
  24.     .cpspot-input kbd{font-size:12px;color:#9aa1b2;border:1px solid #e1e4ea;border-radius:5px;padding:1px 6px;}
  25.     .cpspot-results{flex:1;overflow:auto;padding:6px;}
  26.     .cpspot-empty,.cpspot-loading{padding:34px;text-align:center;color:#9aa1b2;font-size:14px;}
  27.     .cpspot-group{font-size:var(--cp-fs-xs, 11.5px);text-transform:uppercase;letter-spacing:.06em;color:#9aa1b2;padding:8px 12px 4px;}
  28.     .cpspot-item{display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:9px;cursor:pointer;color:#1f2430;text-decoration:none;}
  29.     .cpspot-item:hover,.cpspot-item.active{background:#f7f1ea;}
  30.     .cpspot-item .ci{width:30px;height:30px;border-radius:8px;background:#f5ece1;color:#b06a30;display:flex;align-items:center;justify-content:center;font-size:var(--cp-fs-base, 13px);flex:0 0 auto;}
  31.     .cpspot-item .cn{flex:1;font-size:14px;}
  32.     .cpspot-item .ca{font-size:12px;color:#9aa1b2;}
  33.     .cpspot-item em{background:#fbe9d2;font-style:normal;border-radius:3px;}
  34.     .cpspot-foot{display:flex;gap:16px;justify-content:center;padding:9px;border-top:1px solid #eef0f4;font-size:12px;color:#9aa1b2;}
  35.     .cpspot-foot kbd{border:1px solid #e1e4ea;border-radius:4px;padding:0 5px;}
  36. </style>
  37. <script>
  38. (function(){
  39.     var SPOT_TPL='{{ path('spotlight_search', {queryStr: '__Q__'}) }}';
  40.     var TYPES={module:{i:'fa-th-large',a:'Go'},client:{i:'fa-building',a:'View'},supplier:{i:'fa-truck',a:'View'},document:{i:'fa-file',a:'Open'},employee:{i:'fa-user',a:'View'}};
  41.     var modal=document.getElementById('cpSpot'), input=document.getElementById('cpSpotInput'), res=document.getElementById('cpSpotResults');
  42.     if(!modal) return;
  43.     var idx=-1, timer, xhr;
  44.     function esc(s){return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
  45.     function open(){ modal.classList.add('open'); setTimeout(function(){input.focus();input.select();},60); }
  46.     function close(){ modal.classList.remove('open'); input.value=''; placeholder(); idx=-1; }
  47.     function placeholder(){ res.innerHTML='<div class="cpspot-empty">Type to search across modules, records and documents</div>'; idx=-1; }
  48.     function items(){ return res.querySelectorAll('.cpspot-item'); }
  49.     function move(d){ var it=items(); if(!it.length)return; if(it[idx])it[idx].classList.remove('active'); idx=Math.max(0,Math.min(it.length-1,(idx<0?(d>0?0:it.length-1):idx+d))); it[idx].classList.add('active'); it[idx].scrollIntoView({block:'nearest'}); }
  50.     function render(data,q){
  51.         idx=-1;
  52.         if(!data||!data.length){ res.innerHTML='<div class="cpspot-empty">No results for <b>'+esc(q)+'</b></div>'; return; }
  53.         var groups={},order=[];
  54.         data.forEach(function(d){ var t=d.type||'module'; if(!groups[t]){groups[t]=[];order.push(t);} groups[t].push(d); });
  55.         var html='';
  56.         order.forEach(function(t){
  57.             var meta=TYPES[t]||TYPES.module;
  58.             html+='<div class="cpspot-group">'+t+'s</div>';
  59.             groups[t].forEach(function(d){
  60.                 html+='<a class="cpspot-item" href="'+(d.url||'#')+'"><span class="ci"><i class="fa '+meta.i+'"></i></span>'
  61.                   +'<span class="cn">'+esc(d.name||d.title||'')+'</span><span class="ca">'+meta.a+'</span></a>';
  62.             });
  63.         });
  64.         res.innerHTML=html;
  65.     }
  66.     document.addEventListener('keydown',function(e){
  67.         if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); modal.classList.contains('open')?close():open(); return; }
  68.         if(!modal.classList.contains('open'))return;
  69.         if(e.key==='Escape'){close();} else if(e.key==='ArrowDown'){e.preventDefault();move(1);} else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}
  70.         else if(e.key==='Enter'){e.preventDefault(); var it=items(); if(idx>=0&&it[idx])it[idx].click(); else if(it.length===1)it[0].click();}
  71.     });
  72.     modal.addEventListener('click',function(e){ if(e.target===modal)close(); });
  73.     input.addEventListener('input',function(){
  74.         var q=this.value.trim(); clearTimeout(timer);
  75.         if(!q){ placeholder(); return; }
  76.         res.innerHTML='<div class="cpspot-loading"><i class="fa fa-circle-o-notch fa-spin"></i></div>';
  77.         timer=setTimeout(function(){
  78.             if(xhr)xhr.abort();
  79.             xhr=$.ajax({url:SPOT_TPL.replace('__Q__',encodeURIComponent(q)),type:'GET',
  80.                 success:function(r){ render((r&&r.data)||[],q); },
  81.                 error:function(x){ if(x.statusText!=='abort') res.innerHTML='<div class="cpspot-empty">Search error.</div>'; }});
  82.         },220);
  83.     });
  84.     // expose for an optional topbar button
  85.     window.cpOpenSpotlight=open;
  86. }());
  87. </script>