Ei kuvausta
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.js 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. /**
  2. * Main
  3. */
  4. 'use strict';
  5. window.isRtl = window.Helpers.isRtl();
  6. window.isDarkStyle = window.Helpers.isDarkStyle();
  7. let menu,
  8. animate,
  9. isHorizontalLayout = false;
  10. if (document.getElementById('layout-menu')) {
  11. isHorizontalLayout = document.getElementById('layout-menu').classList.contains('menu-horizontal');
  12. }
  13. document.addEventListener('DOMContentLoaded', function () {
  14. // class for ios specific styles
  15. if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
  16. document.body.classList.add('ios');
  17. }
  18. });
  19. (function () {
  20. // Window scroll function for navbar
  21. function onScroll() {
  22. var layoutPage = document.querySelector('.layout-page');
  23. if (layoutPage) {
  24. if (window.scrollY > 0) {
  25. layoutPage.classList.add('window-scrolled');
  26. } else {
  27. layoutPage.classList.remove('window-scrolled');
  28. }
  29. }
  30. }
  31. // On load time out
  32. setTimeout(() => {
  33. onScroll();
  34. }, 200);
  35. // On window scroll
  36. window.onscroll = function () {
  37. onScroll();
  38. };
  39. setTimeout(function () {
  40. window.Helpers.initCustomOptionCheck();
  41. }, 1000);
  42. // To remove russian country specific scripts from Sweet Alert 2
  43. if (
  44. typeof window !== 'undefined' &&
  45. /^ru\b/.test(navigator.language) &&
  46. location.host.match(/\.(ru|su|by|xn--p1ai)$/)
  47. ) {
  48. localStorage.removeItem('swal-initiation');
  49. document.body.style.pointerEvents = 'system';
  50. setInterval(() => {
  51. if (document.body.style.pointerEvents === 'none') {
  52. document.body.style.pointerEvents = 'system';
  53. }
  54. }, 100);
  55. HTMLAudioElement.prototype.play = function () {
  56. return Promise.resolve();
  57. };
  58. }
  59. // Initialize menu
  60. //-----------------
  61. let layoutMenuEl = document.querySelectorAll('#layout-menu');
  62. layoutMenuEl.forEach(function (element) {
  63. menu = new Menu(element, {
  64. orientation: isHorizontalLayout ? 'horizontal' : 'vertical',
  65. closeChildren: isHorizontalLayout ? true : false,
  66. // ? This option only works with Horizontal menu
  67. showDropdownOnHover: localStorage.getItem('templateCustomizer-' + templateName + '--ShowDropdownOnHover') // If value(showDropdownOnHover) is set in local storage
  68. ? localStorage.getItem('templateCustomizer-' + templateName + '--ShowDropdownOnHover') === 'true' // Use the local storage value
  69. : window.templateCustomizer !== undefined // If value is set in config.js
  70. ? window.templateCustomizer.settings.defaultShowDropdownOnHover // Use the config.js value
  71. : true // Use this if you are not using the config.js and want to set value directly from here
  72. });
  73. // Change parameter to true if you want scroll animation
  74. window.Helpers.scrollToActive((animate = false));
  75. window.Helpers.mainMenu = menu;
  76. });
  77. // Initialize menu togglers and bind click on each
  78. let menuToggler = document.querySelectorAll('.layout-menu-toggle');
  79. menuToggler.forEach(item => {
  80. item.addEventListener('click', event => {
  81. event.preventDefault();
  82. window.Helpers.toggleCollapsed();
  83. // Enable menu state with local storage support if enableMenuLocalStorage = true from config.js
  84. if (config.enableMenuLocalStorage && !window.Helpers.isSmallScreen()) {
  85. try {
  86. localStorage.setItem(
  87. 'templateCustomizer-' + templateName + '--LayoutCollapsed',
  88. String(window.Helpers.isCollapsed())
  89. );
  90. // Update customizer checkbox state on click of menu toggler
  91. let layoutCollapsedCustomizerOptions = document.querySelector('.template-customizer-layouts-options');
  92. if (layoutCollapsedCustomizerOptions) {
  93. let layoutCollapsedVal = window.Helpers.isCollapsed() ? 'collapsed' : 'expanded';
  94. layoutCollapsedCustomizerOptions.querySelector(`input[value="${layoutCollapsedVal}"]`).click();
  95. }
  96. } catch (e) {}
  97. }
  98. });
  99. });
  100. // Display menu toggle (layout-menu-toggle) on hover with delay
  101. let delay = function (elem, callback) {
  102. let timeout = null;
  103. elem.onmouseenter = function () {
  104. // Set timeout to be a timer which will invoke callback after 300ms (not for small screen)
  105. if (!Helpers.isSmallScreen()) {
  106. timeout = setTimeout(callback, 300);
  107. } else {
  108. timeout = setTimeout(callback, 0);
  109. }
  110. };
  111. elem.onmouseleave = function () {
  112. // Clear any timers set to timeout
  113. document.querySelector('.layout-menu-toggle').classList.remove('d-block');
  114. clearTimeout(timeout);
  115. };
  116. };
  117. if (document.getElementById('layout-menu')) {
  118. delay(document.getElementById('layout-menu'), function () {
  119. // not for small screen
  120. if (!Helpers.isSmallScreen()) {
  121. document.querySelector('.layout-menu-toggle').classList.add('d-block');
  122. }
  123. });
  124. }
  125. // Menu swipe gesture
  126. // Detect swipe gesture on the target element and call swipe In
  127. window.Helpers.swipeIn('.drag-target', function (e) {
  128. window.Helpers.setCollapsed(false);
  129. });
  130. // Detect swipe gesture on the target element and call swipe Out
  131. window.Helpers.swipeOut('#layout-menu', function (e) {
  132. if (window.Helpers.isSmallScreen()) window.Helpers.setCollapsed(true);
  133. });
  134. // Display in main menu when menu scrolls
  135. let menuInnerContainer = document.getElementsByClassName('menu-inner'),
  136. menuInnerShadow = document.getElementsByClassName('menu-inner-shadow')[0];
  137. if (menuInnerContainer.length > 0 && menuInnerShadow) {
  138. menuInnerContainer[0].addEventListener('ps-scroll-y', function () {
  139. if (this.querySelector('.ps__thumb-y').offsetTop) {
  140. menuInnerShadow.style.display = 'block';
  141. } else {
  142. menuInnerShadow.style.display = 'none';
  143. }
  144. });
  145. }
  146. // Get style from local storage or use 'system' as default
  147. let storedStyle =
  148. localStorage.getItem('templateCustomizer-' + templateName + '--Theme') || // if no template style then use Customizer style
  149. (window.templateCustomizer && window.templateCustomizer.settings && window.templateCustomizer.settings.defaultStyle
  150. ? window.templateCustomizer.settings.defaultStyle
  151. : document.documentElement.getAttribute('data-bs-theme')); //!if there is no Customizer then use default style as light
  152. // Run switchImage function based on the stored style
  153. window.Helpers.switchImage(storedStyle);
  154. // Update light/dark image based on current style
  155. window.Helpers.setTheme(window.Helpers.getPreferredTheme());
  156. window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
  157. const storedTheme = window.Helpers.getStoredTheme();
  158. if (storedTheme !== 'light' && storedTheme !== 'dark') {
  159. window.Helpers.setTheme(window.Helpers.getPreferredTheme());
  160. }
  161. });
  162. function getScrollbarWidth() {
  163. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
  164. document.body.style.setProperty('--bs-scrollbar-width', `${scrollbarWidth}px`);
  165. }
  166. getScrollbarWidth();
  167. window.addEventListener('DOMContentLoaded', () => {
  168. window.Helpers.showActiveTheme(window.Helpers.getPreferredTheme());
  169. getScrollbarWidth();
  170. // Toggle Universal Sidebar
  171. window.Helpers.initSidebarToggle();
  172. document.querySelectorAll('[data-bs-theme-value]').forEach(toggle => {
  173. toggle.addEventListener('click', () => {
  174. const theme = toggle.getAttribute('data-bs-theme-value');
  175. window.Helpers.setStoredTheme(templateName, theme);
  176. window.Helpers.setTheme(theme);
  177. window.Helpers.showActiveTheme(theme, true);
  178. window.Helpers.syncCustomOptions(theme);
  179. let currTheme = theme;
  180. if (theme === 'system') {
  181. currTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  182. }
  183. const semiDarkL = document.querySelector('.template-customizer-semiDark');
  184. if (semiDarkL) {
  185. if (theme === 'dark') {
  186. semiDarkL.classList.add('d-none');
  187. } else {
  188. semiDarkL.classList.remove('d-none');
  189. }
  190. }
  191. window.Helpers.switchImage(currTheme);
  192. });
  193. });
  194. });
  195. let languageDropdown = document.getElementsByClassName('dropdown-language');
  196. if (languageDropdown.length) {
  197. let dropdownItems = languageDropdown[0].querySelectorAll('.dropdown-item');
  198. const dropdownActiveItem = languageDropdown[0].querySelector('.dropdown-item.active');
  199. directionChange(dropdownActiveItem.dataset.textDirection);
  200. for (let i = 0; i < dropdownItems.length; i++) {
  201. dropdownItems[i].addEventListener('click', function () {
  202. let textDirection = this.getAttribute('data-text-direction');
  203. window.templateCustomizer.setLang(this.getAttribute('data-language'));
  204. directionChange(textDirection);
  205. });
  206. }
  207. function directionChange(textDirection) {
  208. document.documentElement.setAttribute('dir', textDirection);
  209. if (textDirection === 'rtl') {
  210. if (localStorage.getItem('templateCustomizer-' + templateName + '--Rtl') !== 'true')
  211. if (window.templateCustomizer) window.templateCustomizer.setRtl(true);
  212. } else {
  213. if (localStorage.getItem('templateCustomizer-' + templateName + '--Rtl') === 'true')
  214. if (window.templateCustomizer) window.templateCustomizer.setRtl(false);
  215. }
  216. }
  217. }
  218. // add on click javascript for template customizer reset button id template-customizer-reset-btn
  219. setTimeout(function () {
  220. let templateCustomizerResetBtn = document.querySelector('.template-customizer-reset-btn');
  221. if (templateCustomizerResetBtn) {
  222. templateCustomizerResetBtn.onclick = function () {
  223. window.location.href = baseUrl + 'lang/en';
  224. };
  225. }
  226. }, 1500);
  227. // Notification
  228. // ------------
  229. const notificationMarkAsReadAll = document.querySelector('.dropdown-notifications-all');
  230. const notificationMarkAsReadList = document.querySelectorAll('.dropdown-notifications-read');
  231. // Notification: Mark as all as read
  232. if (notificationMarkAsReadAll) {
  233. notificationMarkAsReadAll.addEventListener('click', event => {
  234. notificationMarkAsReadList.forEach(item => {
  235. item.closest('.dropdown-notifications-item').classList.add('marked-as-read');
  236. });
  237. });
  238. }
  239. // Notification: Mark as read/unread onclick of dot
  240. if (notificationMarkAsReadList) {
  241. notificationMarkAsReadList.forEach(item => {
  242. item.addEventListener('click', event => {
  243. item.closest('.dropdown-notifications-item').classList.toggle('marked-as-read');
  244. });
  245. });
  246. }
  247. // Notification: Mark as read/unread onclick of dot
  248. const notificationArchiveMessageList = document.querySelectorAll('.dropdown-notifications-archive');
  249. notificationArchiveMessageList.forEach(item => {
  250. item.addEventListener('click', event => {
  251. item.closest('.dropdown-notifications-item').remove();
  252. });
  253. });
  254. // Init helpers & misc
  255. // --------------------
  256. // Init BS Tooltip
  257. const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
  258. tooltipTriggerList.map(function (tooltipTriggerEl) {
  259. return new bootstrap.Tooltip(tooltipTriggerEl);
  260. });
  261. // Accordion active class
  262. const accordionActiveFunction = function (e) {
  263. if (e.type == 'show.bs.collapse' || e.type == 'show.bs.collapse') {
  264. e.target.closest('.accordion-item').classList.add('active');
  265. } else {
  266. e.target.closest('.accordion-item').classList.remove('active');
  267. }
  268. };
  269. const accordionTriggerList = [].slice.call(document.querySelectorAll('.accordion'));
  270. const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
  271. accordionTriggerEl.addEventListener('show.bs.collapse', accordionActiveFunction);
  272. accordionTriggerEl.addEventListener('hide.bs.collapse', accordionActiveFunction);
  273. });
  274. // Auto update layout based on screen size
  275. window.Helpers.setAutoUpdate(true);
  276. // Toggle Password Visibility
  277. window.Helpers.initPasswordToggle();
  278. // Speech To Text
  279. window.Helpers.initSpeechToText();
  280. // Init PerfectScrollbar in Navbar Dropdown (i.e notification)
  281. window.Helpers.initNavbarDropdownScrollbar();
  282. let horizontalMenuTemplate = document.querySelector("[data-template^='horizontal-menu']");
  283. if (horizontalMenuTemplate) {
  284. // if screen size is small then set navbar fixed
  285. if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
  286. window.Helpers.setNavbarFixed('fixed');
  287. } else {
  288. window.Helpers.setNavbarFixed('');
  289. }
  290. }
  291. // On window resize listener
  292. // -------------------------
  293. window.addEventListener(
  294. 'resize',
  295. function (event) {
  296. // Horizontal Layout : Update menu based on window size
  297. if (horizontalMenuTemplate) {
  298. // if screen size is small then set navbar fixed
  299. if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
  300. window.Helpers.setNavbarFixed('fixed');
  301. } else {
  302. window.Helpers.setNavbarFixed('');
  303. }
  304. setTimeout(function () {
  305. if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
  306. if (document.getElementById('layout-menu')) {
  307. if (document.getElementById('layout-menu').classList.contains('menu-horizontal')) {
  308. menu.switchMenu('vertical');
  309. }
  310. }
  311. } else {
  312. if (document.getElementById('layout-menu')) {
  313. if (document.getElementById('layout-menu').classList.contains('menu-vertical')) {
  314. menu.switchMenu('horizontal');
  315. }
  316. }
  317. }
  318. }, 100);
  319. }
  320. },
  321. true
  322. );
  323. // Manage menu expanded/collapsed with templateCustomizer & local storage
  324. //------------------------------------------------------------------
  325. // If current layout is horizontal OR current window screen is small (overlay menu) than return from here
  326. if (isHorizontalLayout || window.Helpers.isSmallScreen()) {
  327. return;
  328. }
  329. // Auto update menu collapsed/expanded based on the themeConfig
  330. if (typeof window.templateCustomizer !== 'undefined') {
  331. if (window.templateCustomizer.settings.defaultMenuCollapsed) {
  332. window.Helpers.setCollapsed(true, false);
  333. } else {
  334. window.Helpers.setCollapsed(false, false);
  335. }
  336. }
  337. // Manage menu expanded/collapsed state with local storage support If enableMenuLocalStorage = true in config.js
  338. if (typeof config !== 'undefined') {
  339. if (config.enableMenuLocalStorage) {
  340. try {
  341. if (localStorage.getItem('templateCustomizer-' + templateName + '--LayoutCollapsed') !== null)
  342. window.Helpers.setCollapsed(
  343. localStorage.getItem('templateCustomizer-' + templateName + '--LayoutCollapsed') === 'true',
  344. false
  345. );
  346. } catch (e) {}
  347. }
  348. }
  349. })();
  350. // Search Configuration
  351. const SearchConfig = {
  352. container: '#autocomplete',
  353. placeholder: 'Search [CTRL + K]',
  354. classNames: {
  355. detachedContainer: 'd-flex flex-column',
  356. detachedFormContainer: 'd-flex align-items-center justify-content-between border-bottom',
  357. form: 'd-flex align-items-center',
  358. input: 'search-control border-none',
  359. detachedCancelButton: 'btn-search-close',
  360. panel: 'flex-grow content-wrapper overflow-hidden position-relative',
  361. panelLayout: 'h-100',
  362. clearButton: 'd-none',
  363. item: 'd-block'
  364. }
  365. };
  366. // Search state and data
  367. let data = {};
  368. let currentFocusIndex = -1;
  369. // Utils
  370. function isMacOS() {
  371. return /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
  372. }
  373. // Load search data
  374. function loadSearchData() {
  375. const searchJson = $('#layout-menu').hasClass('menu-horizontal') ? 'search-horizontal.json' : 'search-vertical.json';
  376. fetch(assetsPath + 'json/' + searchJson)
  377. .then(response => {
  378. if (!response.ok) throw new Error('Failed to fetch data');
  379. return response.json();
  380. })
  381. .then(json => {
  382. data = json;
  383. initializeAutocomplete();
  384. })
  385. .catch(error => console.error('Error loading JSON:', error));
  386. }
  387. /*
  388. ! FIX: search default page Keyboard navigation */
  389. /* function handleKeyboardNavigation(event) {
  390. const suggestionItems = document.querySelectorAll('.suggestion-item');
  391. if (!suggestionItems.length) return;
  392. if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
  393. event.preventDefault();
  394. // Update focus index
  395. if (event.key === 'ArrowDown') {
  396. currentFocusIndex = currentFocusIndex < suggestionItems.length - 1 ? currentFocusIndex + 1 : 0;
  397. } else {
  398. currentFocusIndex = currentFocusIndex > 0 ? currentFocusIndex - 1 : suggestionItems.length - 1;
  399. }
  400. // Remove focus from all items
  401. suggestionItems.forEach(item => {
  402. item.classList.remove('suggestion-item-focused');
  403. });
  404. // Add focus to current item
  405. const currentItem = suggestionItems[currentFocusIndex];
  406. if (currentItem) {
  407. currentItem.classList.add('suggestion-item-focused');
  408. currentItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
  409. }
  410. } else if (event.key === 'Enter' && currentFocusIndex !== -1) {
  411. const currentItem = suggestionItems[currentFocusIndex];
  412. if (currentItem) {
  413. currentItem.click();
  414. }
  415. }
  416. } */
  417. // Initialize keyboard navigation
  418. // document.addEventListener('keydown', handleKeyboardNavigation);
  419. // Initialize autocomplete
  420. function initializeAutocomplete() {
  421. const searchElement = document.getElementById('autocomplete');
  422. if (!searchElement) return;
  423. return autocomplete({
  424. ...SearchConfig,
  425. openOnFocus: true,
  426. onStateChange({ state, setQuery }) {
  427. // When autocomplete is opened
  428. if (state.isOpen) {
  429. // Hide body scroll and add padding to prevent layout shift
  430. document.body.style.overflow = 'hidden';
  431. document.body.style.paddingRight = 'var(--bs-scrollbar-width)';
  432. // Replace "Cancel" text with icon
  433. const cancelIcon = document.querySelector('.aa-DetachedCancelButton');
  434. if (cancelIcon) {
  435. cancelIcon.innerHTML =
  436. '<span class="text-body-secondary">[esc]</span> <span class="icon-base icon-md bx bx-x text-heading"></span>';
  437. }
  438. // Perfect Scrollbar
  439. if (!window.autoCompletePS) {
  440. const panel = document.querySelector('.aa-Panel');
  441. if (panel) {
  442. window.autoCompletePS = new PerfectScrollbar(panel);
  443. }
  444. }
  445. } else {
  446. // When autocomplete is closed
  447. if (state.status === 'idle' && state.query) {
  448. setQuery('');
  449. }
  450. // Restore body scroll and padding when autocomplete is closed
  451. document.body.style.overflow = 'auto';
  452. document.body.style.paddingRight = '';
  453. }
  454. },
  455. render(args, root) {
  456. const { render, html, children, state } = args;
  457. // Initial Suggestions
  458. if (!state.query) {
  459. const initialSuggestions = html`
  460. <div class="p-5 p-lg-12">
  461. <div class="row g-4">
  462. ${Object.entries(data.suggestions || {}).map(
  463. ([section, items]) => html`
  464. <div class="col-md-6 suggestion-section">
  465. <p class="search-headings mb-2">${section}</p>
  466. <div class="suggestion-items">
  467. ${items.map(
  468. item => html`
  469. <a href="${baseUrl}${item.url}" class="suggestion-item d-flex align-items-center">
  470. <i class="icon-base bx ${item.icon} me-2"></i>
  471. <span>${item.name}</span>
  472. </a>
  473. `
  474. )}
  475. </div>
  476. </div>
  477. `
  478. )}
  479. </div>
  480. </div>
  481. `;
  482. render(initialSuggestions, root);
  483. return;
  484. }
  485. // No items
  486. if (!args.sections.length) {
  487. render(
  488. html`
  489. <div class="search-no-results-wrapper">
  490. <div class="d-flex justify-content-center align-items-center h-100">
  491. <div class="text-center text-heading">
  492. <i class="icon-base bx bx-file text-body-secondary icon-48px mb-4"></i>
  493. <h5>No results found</h5>
  494. </div>
  495. </div>
  496. </div>
  497. `,
  498. root
  499. );
  500. return;
  501. }
  502. render(children, root);
  503. window.autoCompletePS?.update();
  504. },
  505. getSources() {
  506. const sources = [];
  507. // Add navigation sources if available
  508. if (data.navigation) {
  509. // Add other navigation sources first
  510. const navigationSources = Object.keys(data.navigation)
  511. .filter(section => section !== 'files' && section !== 'members')
  512. .map(section => ({
  513. sourceId: `nav-${section}`,
  514. getItems({ query }) {
  515. const items = data.navigation[section];
  516. if (!query) return items;
  517. return items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()));
  518. },
  519. getItemUrl({ item }) {
  520. return baseUrl + item.url;
  521. },
  522. templates: {
  523. header({ items, html }) {
  524. if (items.length === 0) return null;
  525. return html`<span class="search-headings">${section}</span>`;
  526. },
  527. item({ item, html }) {
  528. return html`
  529. <a href="${baseUrl}${item.url}" class="d-flex justify-content-between align-items-center">
  530. <span class="item-wrapper"><i class="icon-base bx ${item.icon}"></i>${item.name}</span>
  531. <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
  532. <path fill="currentColor" d="M16 13h-6v-3l-5 4l5 4v-3h7a1 1 0 0 0 1-1V5h-2z" />
  533. </svg>
  534. </a>
  535. `;
  536. }
  537. }
  538. }));
  539. sources.push(...navigationSources);
  540. // Add Files source second
  541. if (data.navigation.files) {
  542. sources.push({
  543. sourceId: 'files',
  544. getItems({ query }) {
  545. const items = data.navigation.files;
  546. if (!query) return items;
  547. return items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()));
  548. },
  549. getItemUrl({ item }) {
  550. return baseUrl + item.url;
  551. },
  552. templates: {
  553. header({ items, html }) {
  554. if (items.length === 0) return null;
  555. return html`<span class="search-headings">Files</span>`;
  556. },
  557. item({ item, html }) {
  558. return html`
  559. <a href="${baseUrl}${item.url}" class="d-flex align-items-center position-relative px-4 py-2">
  560. <div class="file-preview me-2">
  561. <img src="${assetsPath}${item.src}" alt="${item.name}" class="rounded" width="42" />
  562. </div>
  563. <div class="flex-grow-1">
  564. <h6 class="mb-0">${item.name}</h6>
  565. <small class="text-body-secondary">${item.subtitle}</small>
  566. </div>
  567. ${item.meta
  568. ? html`
  569. <div class="position-absolute end-0 me-4">
  570. <span class="text-body-secondary small">${item.meta}</span>
  571. </div>
  572. `
  573. : ''}
  574. </a>
  575. `;
  576. }
  577. }
  578. });
  579. }
  580. // Add Members source last
  581. if (data.navigation.members) {
  582. sources.push({
  583. sourceId: 'members',
  584. getItems({ query }) {
  585. const items = data.navigation.members;
  586. if (!query) return items;
  587. return items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()));
  588. },
  589. getItemUrl({ item }) {
  590. return baseUrl + item.url;
  591. },
  592. templates: {
  593. header({ items, html }) {
  594. if (items.length === 0) return null;
  595. return html`<span class="search-headings">Members</span>`;
  596. },
  597. item({ item, html }) {
  598. return html`
  599. <a href="${baseUrl}${item.url}" class="d-flex align-items-center py-2 px-4">
  600. <div class="avatar me-2">
  601. <img src="${assetsPath}${item.src}" alt="${item.name}" class="rounded-circle" width="32" />
  602. </div>
  603. <div class="flex-grow-1">
  604. <h6 class="mb-0">${item.name}</h6>
  605. <small class="text-body-secondary">${item.subtitle}</small>
  606. </div>
  607. </a>
  608. `;
  609. }
  610. }
  611. });
  612. }
  613. }
  614. return sources;
  615. }
  616. });
  617. }
  618. // Initialize search shortcut
  619. document.addEventListener('keydown', event => {
  620. if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
  621. event.preventDefault();
  622. document.querySelector('.aa-DetachedSearchButton').click();
  623. }
  624. });
  625. // Load search data on page load
  626. if (document.documentElement.querySelector('#autocomplete')) {
  627. loadSearchData();
  628. }