1document.addEventListener('DOMContentLoaded', function () { 2 // Function to initialize fuzzy search for a given input or textarea element 3 function initializeFuzzySearch(element) { 4 if (!element) { 5 console.error('Element not found!'); 6 return; 7 } 8 9 let resultsDiv = null; 10 let currentIndex = -1; 11 let lastPhrase = ''; 12 let fuse = null; 13 let pagesCache = null; 14 let searchTimeout = null; 15 16 // Fetch pages and initialize Fuse.js 17 fetch(DOKU_BASE + 'lib/exe/ajax.php?call=fuzzysearch_pages', { 18 method: 'GET', 19 credentials: 'same-origin' 20 }) 21 .then(response => { 22 if (!response.ok) throw new Error('Failed to fetch pages'); 23 return response.json(); 24 }) 25 .then(pages => { 26 pagesCache = pages; 27 fuse = new Fuse(pagesCache, { 28 keys: ['title'], 29 threshold: 0.4, 30 includeScore: true, 31 maxPatternLength: 32, 32 minMatchCharLength: 2 33 }); 34 }) 35 .catch(error => console.error('Initial fetch error:', error)); 36 37 // Function to handle search logic 38 function handleInputChange() { 39 if (searchTimeout) clearTimeout(searchTimeout); 40 41 searchTimeout = setTimeout(() => { 42 const cursorPos = element.selectionStart || element.value.length; 43 const text = element.value; 44 const match = text.substring(0, cursorPos).match(/\[\[([^\[\]]+)\]\]$/); 45 if (!match) { 46 if (resultsDiv) { 47 resultsDiv.remove(); 48 resultsDiv = null; 49 } 50 return; 51 } 52 53 const phrase = match[1].trim(); 54 if (!phrase || phrase === lastPhrase) { 55 return; 56 } 57 lastPhrase = phrase; 58 59 if (!fuse) { 60 console.error('Fuse not initialized yet'); 61 return; 62 } 63 64 const results = fuse.search(phrase, { limit: 10 }); 65 displayResults(results, phrase, cursorPos); 66 }, 300); // Debounce delay 67 } 68 69 // Add event listeners 70 element.addEventListener('keyup', function (e) { 71 if (e.key === ']') { 72 handleInputChange(); 73 } 74 }); 75 76 element.addEventListener('input', handleInputChange); 77 78 element.addEventListener('compositionend', handleInputChange); 79 80 // Display results function 81 function displayResults(results, phrase, cursorPos) { 82 if (resultsDiv) { 83 resultsDiv.remove(); 84 } 85 if (results.length === 0) { 86 return; 87 } 88 89 resultsDiv = document.createElement('div'); 90 resultsDiv.id = 'fuzzysearch-editor-results'; 91 resultsDiv.style.position = 'absolute'; 92 resultsDiv.style.background = 'white'; 93 resultsDiv.style.border = '1px solid #ccc'; 94 resultsDiv.style.padding = '5px'; 95 resultsDiv.style.zIndex = '1000'; 96 resultsDiv.style.maxHeight = '200px'; 97 resultsDiv.style.overflowY = 'auto'; 98 99 const elementRect = element.getBoundingClientRect(); 100 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 101 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 102 const coords = getCaretCoordinates(element, cursorPos); 103 104 const cursorTop = elementRect.top + scrollTop + coords.top; 105 const cursorLeft = elementRect.left + scrollLeft + coords.left; 106 107 resultsDiv.style.left = cursorLeft + 'px'; 108 let desiredTop = cursorTop; 109 110 results.forEach((result, index) => { 111 const page = result.item; 112 const div = document.createElement('div'); 113 div.textContent = page.title; 114 div.dataset.index = index; 115 div.dataset.id = page.id; 116 div.style.cursor = 'pointer'; 117 div.style.padding = '2px 5px'; 118 div.addEventListener('mouseover', () => highlightResult(index)); 119 div.addEventListener('click', () => selectResult(page.id, phrase)); 120 resultsDiv.appendChild(div); 121 }); 122 123 document.body.appendChild(resultsDiv); 124 const dropdownHeight = resultsDiv.offsetHeight; 125 const elementBottom = elementRect.bottom + scrollTop; 126 127 if (desiredTop + dropdownHeight > elementBottom) { 128 desiredTop = elementBottom - dropdownHeight; 129 } 130 131 const viewportHeight = window.innerHeight; 132 const maxTop = scrollTop + viewportHeight - dropdownHeight; 133 if (desiredTop > maxTop) { 134 desiredTop = maxTop; 135 } 136 137 const elementTop = elementRect.top + scrollTop; 138 if (desiredTop < elementTop) { 139 desiredTop = elementTop; 140 } 141 142 resultsDiv.style.top = desiredTop + 'px'; 143 144 currentIndex = 0; 145 highlightResult(currentIndex); 146 } 147 148 function highlightResult(index) { 149 if (!resultsDiv) return; 150 const items = resultsDiv.children; 151 for (let i = 0; i < items.length; i++) { 152 items[i].style.background = i === index ? '#ddd' : 'white'; 153 } 154 currentIndex = index; 155 if (items[index]) { 156 items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); 157 } 158 } 159 160 function selectResult(pageId, phrase) { 161 const text = element.value; 162 const newLink = `[[${pageId}|${phrase}]]`; 163 const start = text.lastIndexOf(`[[${phrase}]]`); 164 element.value = text.substring(0, start) + newLink + text.substring(start + phrase.length + 4); 165 if (resultsDiv) { 166 resultsDiv.remove(); 167 resultsDiv = null; 168 } 169 lastPhrase = ''; 170 element.focus(); 171 } 172 173 element.addEventListener('keydown', function (e) { 174 if (!resultsDiv || resultsDiv.children.length === 0) return; 175 176 if (e.key === 'ArrowDown') { 177 e.preventDefault(); 178 if (currentIndex < resultsDiv.children.length - 1) { 179 currentIndex++; 180 highlightResult(currentIndex); 181 } 182 } else if (e.key === 'ArrowUp') { 183 e.preventDefault(); 184 if (currentIndex > 0) { 185 currentIndex--; 186 highlightResult(currentIndex); 187 } 188 } else if (e.key === 'Space' || e.key === 'Enter') { 189 e.preventDefault(); 190 if (currentIndex >= 0) { 191 const selected = resultsDiv.children[currentIndex]; 192 selectResult(selected.dataset.id, lastPhrase); 193 } 194 } else if (e.key === 'Escape') { 195 if (resultsDiv) { 196 resultsDiv.remove(); 197 resultsDiv = null; 198 lastPhrase = ''; 199 } 200 } 201 }); 202 203 document.addEventListener('click', function (e) { 204 if (resultsDiv && !resultsDiv.contains(e.target) && e.target !== element) { 205 resultsDiv.remove(); 206 resultsDiv = null; 207 lastPhrase = ''; 208 } 209 }); 210 211 function getCaretCoordinates(element, position) { 212 const isTextarea = element.tagName.toLowerCase() === 'textarea'; 213 const text = element.value.substring(0, position); 214 const font = window.getComputedStyle(element).font; 215 const canvas = document.createElement('canvas'); 216 const context = canvas.getContext('2d'); 217 context.font = font; 218 219 if (isTextarea) { 220 const lines = text.split('\n'); 221 const lastLine = lines[lines.length - 1]; 222 const width = context.measureText(lastLine).width; 223 const lineHeight = parseInt(font); 224 const top = (lines.length - 1) * lineHeight; 225 return { top: top, left: width }; 226 } else { 227 // For input type="text", no line breaks, just measure the text width 228 const width = context.measureText(text).width; 229 const lineHeight = parseInt(font); 230 return { top: 0, left: width }; 231 } 232 } 233 } 234 235 // Target the main wiki editor textarea 236 const wikiTextarea = document.querySelector('textarea[name="wikitext"]'); 237 if (wikiTextarea) { 238 initializeFuzzySearch(wikiTextarea); 239 } 240 241 // Target Bureaucracy form textareas and textboxes 242 const bureaucracyElements = document.querySelectorAll('.bureaucracy__plugin textarea, .bureaucracy__plugin input[type="text"]'); 243 bureaucracyElements.forEach(element => { 244 initializeFuzzySearch(element); 245 }); 246 247 // Use MutationObserver to catch dynamically added Bureaucracy form elements 248 const observer = new MutationObserver((mutations) => { 249 mutations.forEach((mutation) => { 250 const newElements = mutation.target.querySelectorAll('.bureaucracy__plugin textarea, .bureaucracy__plugin input[type="text"]'); 251 newElements.forEach(element => { 252 if (!element.dataset.fuzzyInitialized) { 253 initializeFuzzySearch(element); 254 element.dataset.fuzzyInitialized = 'true'; // Mark as initialized 255 } 256 }); 257 }); 258 }); 259 260 observer.observe(document.body, { 261 childList: true, 262 subtree: true 263 }); 264});