1
0

main.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // Language selector
  2. (function () {
  3. var btn = document.querySelector('.lang-btn');
  4. var popup = document.querySelector('.lang-popup');
  5. if (!btn || !popup) return;
  6. btn.addEventListener('click', function (e) {
  7. e.stopPropagation();
  8. popup.classList.toggle('open');
  9. });
  10. document.addEventListener('click', function () {
  11. popup.classList.remove('open');
  12. });
  13. popup.addEventListener('click', function (e) {
  14. var link = e.target.closest('[data-lang]');
  15. if (!link) return;
  16. e.preventDefault();
  17. var lang = link.getAttribute('data-lang');
  18. localStorage.setItem('preferred-lang', lang);
  19. var basePath = document.documentElement.getAttribute('data-base-path') || '';
  20. var path = window.location.pathname;
  21. // Strip base path prefix, replace lang, then re-add base path
  22. var pathWithoutBase = path.slice(basePath.length);
  23. var newPath = basePath + pathWithoutBase.replace(/^\/[a-z]{2}\//, '/' + lang + '/');
  24. window.location.href = newPath;
  25. });
  26. })();
  27. // Theme toggle
  28. (function () {
  29. var btn = document.querySelector('.theme-toggle');
  30. if (!btn) return;
  31. // Feather Icons: sun (light mode) and moon (dark mode)
  32. var sunSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
  33. var moonSVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
  34. function getTheme() {
  35. var stored = localStorage.getItem('preferred-theme');
  36. if (stored) return stored;
  37. return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
  38. }
  39. function applyTheme(theme) {
  40. if (theme === 'light') {
  41. document.documentElement.setAttribute('data-theme', 'light');
  42. } else {
  43. document.documentElement.removeAttribute('data-theme');
  44. }
  45. btn.innerHTML = theme === 'light' ? sunSVG : moonSVG;
  46. }
  47. applyTheme(getTheme());
  48. btn.addEventListener('click', function () {
  49. var current = getTheme();
  50. var next = current === 'dark' ? 'light' : 'dark';
  51. localStorage.setItem('preferred-theme', next);
  52. applyTheme(next);
  53. });
  54. })();
  55. // Mobile sidebar toggle
  56. (function () {
  57. var toggle = document.querySelector('.sidebar-toggle');
  58. var sidebar = document.querySelector('.sidebar');
  59. if (!toggle || !sidebar) return;
  60. toggle.addEventListener('click', function () {
  61. sidebar.classList.toggle('open');
  62. });
  63. document.addEventListener('click', function (e) {
  64. if (!sidebar.contains(e.target) && e.target !== toggle) {
  65. sidebar.classList.remove('open');
  66. }
  67. });
  68. })();
  69. // Site search (⌘K / Ctrl+K)
  70. (function () {
  71. var overlay = document.getElementById('search-overlay');
  72. var input = document.getElementById('search-input');
  73. var resultsList = document.getElementById('search-results');
  74. if (!overlay || !input || !resultsList) return;
  75. var searchBtn = document.querySelector('.search-btn');
  76. var pagesData = null; // cached pages-data.json
  77. var activeIndex = -1;
  78. function getCurrentLang() {
  79. return document.documentElement.getAttribute('lang') || 'en';
  80. }
  81. function getBasePath() {
  82. return document.documentElement.getAttribute('data-base-path') || '';
  83. }
  84. function openSearch() {
  85. overlay.classList.add('open');
  86. input.value = '';
  87. resultsList.innerHTML = '';
  88. activeIndex = -1;
  89. input.focus();
  90. loadPagesData();
  91. }
  92. function closeSearch() {
  93. overlay.classList.remove('open');
  94. input.value = '';
  95. resultsList.innerHTML = '';
  96. activeIndex = -1;
  97. }
  98. function loadPagesData() {
  99. if (pagesData) return;
  100. var basePath = getBasePath();
  101. fetch(basePath + '/pages-data.json')
  102. .then(function (res) { return res.json(); })
  103. .then(function (data) { pagesData = data; })
  104. .catch(function () { pagesData = []; });
  105. }
  106. function escapeRegExp(s) {
  107. return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  108. }
  109. function highlightText(text, query) {
  110. if (!query) return text;
  111. var escaped = escapeRegExp(query);
  112. var re = new RegExp('(' + escaped + ')', 'gi');
  113. return text.replace(re, '<mark>$1</mark>');
  114. }
  115. function buildSnippet(body, query) {
  116. if (!query || !body) return '';
  117. var lower = body.toLowerCase();
  118. var idx = lower.indexOf(query.toLowerCase());
  119. var start, end, snippet;
  120. if (idx === -1) {
  121. snippet = body.substring(0, 120);
  122. } else {
  123. start = Math.max(0, idx - 40);
  124. end = Math.min(body.length, idx + query.length + 80);
  125. snippet = (start > 0 ? '...' : '') + body.substring(start, end) + (end < body.length ? '...' : '');
  126. }
  127. return highlightText(snippet, query);
  128. }
  129. function search(query) {
  130. if (!pagesData || !query) {
  131. resultsList.innerHTML = '';
  132. activeIndex = -1;
  133. return;
  134. }
  135. var lang = getCurrentLang();
  136. var q = query.toLowerCase();
  137. // Score and filter
  138. var scored = [];
  139. for (var i = 0; i < pagesData.length; i++) {
  140. var page = pagesData[i];
  141. if (page.lang !== lang) continue;
  142. var score = 0;
  143. var titleLower = page.title.toLowerCase();
  144. var bodyLower = (page.body || '').toLowerCase();
  145. if (titleLower.indexOf(q) !== -1) {
  146. score += 10;
  147. // Bonus for exact title match
  148. if (titleLower === q) score += 5;
  149. }
  150. if (bodyLower.indexOf(q) !== -1) {
  151. score += 3;
  152. }
  153. if (page.section.toLowerCase().indexOf(q) !== -1) {
  154. score += 1;
  155. }
  156. if (score > 0) {
  157. scored.push({ page: page, score: score });
  158. }
  159. }
  160. // Sort by score descending
  161. scored.sort(function (a, b) { return b.score - a.score; });
  162. // Limit results
  163. var results = scored.slice(0, 20);
  164. if (results.length === 0) {
  165. resultsList.innerHTML = '<li class="search-no-results">No results found.</li>';
  166. activeIndex = -1;
  167. return;
  168. }
  169. var html = '';
  170. for (var j = 0; j < results.length; j++) {
  171. var r = results[j];
  172. var snippet = buildSnippet(r.page.body, query);
  173. html += '<li data-url="' + r.page.url + '">'
  174. + '<div class="search-result-title">' + highlightText(r.page.title, query) + '</div>'
  175. + (snippet ? '<div class="search-result-snippet">' + snippet + '</div>' : '')
  176. + '</li>';
  177. }
  178. resultsList.innerHTML = html;
  179. activeIndex = -1;
  180. }
  181. function setActive(index) {
  182. var items = resultsList.querySelectorAll('li[data-url]');
  183. if (items.length === 0) return;
  184. // Remove previous active
  185. for (var i = 0; i < items.length; i++) {
  186. items[i].classList.remove('active');
  187. }
  188. if (index < 0) index = items.length - 1;
  189. if (index >= items.length) index = 0;
  190. activeIndex = index;
  191. items[activeIndex].classList.add('active');
  192. items[activeIndex].scrollIntoView({ block: 'nearest' });
  193. }
  194. function navigateToActive() {
  195. var items = resultsList.querySelectorAll('li[data-url]');
  196. if (activeIndex >= 0 && activeIndex < items.length) {
  197. var url = items[activeIndex].getAttribute('data-url');
  198. if (url) {
  199. closeSearch();
  200. window.location.href = url;
  201. }
  202. }
  203. }
  204. // Event: search button
  205. if (searchBtn) {
  206. searchBtn.addEventListener('click', function (e) {
  207. e.stopPropagation();
  208. openSearch();
  209. });
  210. }
  211. // Use capture phase to intercept keys before browser default behavior
  212. // (e.g. ESC clearing input text in some browsers)
  213. document.addEventListener('keydown', function (e) {
  214. if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
  215. e.preventDefault();
  216. overlay.classList.contains('open') ? closeSearch() : openSearch();
  217. return;
  218. }
  219. if (!overlay.classList.contains('open')) return;
  220. if (e.key === 'Escape') {
  221. e.preventDefault();
  222. closeSearch();
  223. } else if (e.key === 'ArrowDown') {
  224. e.preventDefault();
  225. setActive(activeIndex + 1);
  226. } else if (e.key === 'ArrowUp') {
  227. e.preventDefault();
  228. setActive(activeIndex - 1);
  229. } else if (e.key === 'Enter') {
  230. e.preventDefault();
  231. navigateToActive();
  232. }
  233. }, true); // capture phase
  234. // Event: click overlay background to close
  235. overlay.addEventListener('click', function (e) {
  236. if (e.target === overlay) {
  237. closeSearch();
  238. }
  239. });
  240. // Event: click result item
  241. resultsList.addEventListener('click', function (e) {
  242. var li = e.target.closest('li[data-url]');
  243. if (!li) return;
  244. var url = li.getAttribute('data-url');
  245. if (url) {
  246. closeSearch();
  247. window.location.href = url;
  248. }
  249. });
  250. // Event: input for live search
  251. var debounceTimer = null;
  252. input.addEventListener('input', function () {
  253. clearTimeout(debounceTimer);
  254. debounceTimer = setTimeout(function () {
  255. search(input.value.trim());
  256. }, 150);
  257. });
  258. })();