1document.addEventListener('DOMContentLoaded', function () { 2 //console.log('FuzzySearch editor enhancement loaded'); 3 4 const textarea = document.querySelector('textarea[name="wikitext"]'); 5 if (!textarea) { 6 console.error('Editor textarea not found!'); 7 return; 8 } 9 //console.log('Textarea found:', textarea); 10 11 let resultsDiv = null; 12 let currentIndex = -1; 13 let lastPhrase = ''; 14 let fuse = null; 15 let pagesCache = null; 16 let searchTimeout = null; 17 18 fetch(DOKU_BASE + 'lib/exe/ajax.php?call=fuzzysearch_pages', { 19 method: 'GET', 20 credentials: 'same-origin' 21 }) 22 .then(response => { 23 //console.log('Initial fetch status:', response.status); 24 if (!response.ok) throw new Error('Failed to fetch pages'); 25 return response.json(); 26 }) 27 .then(pages => { 28 pagesCache = pages; 29 //console.log('Pages cached:', pagesCache.length); 30 fuse = new Fuse(pagesCache, { 31 keys: ['title'], 32 threshold: 0.4, 33 includeScore: true, 34 maxPatternLength: 32, 35 minMatchCharLength: 2 36 }); 37 //console.log('Fuse initialized'); 38 }) 39 .catch(error => console.error('Initial fetch error:', error)); 40 41 textarea.addEventListener('keyup', function (e) { 42 //console.log('Keyup detected:', e.key); 43 if (e.key !== ']') return; 44 45 if (searchTimeout) clearTimeout(searchTimeout); 46 47 searchTimeout = setTimeout(() => { 48 const cursorPos = textarea.selectionStart; 49 const text = textarea.value; 50 const match = text.substring(0, cursorPos).match(/\[\[([^\[\]]+)\]\]$/); 51 if (!match) { 52 //console.log('No [[phrase]] pattern found'); 53 return; 54 } 55 56 const phrase = match[1].trim(); 57 if (!phrase || phrase === lastPhrase) { 58 //console.log('Phrase empty or unchanged:', phrase); 59 return; 60 } 61 lastPhrase = phrase; 62 63 if (!fuse) { 64 console.error('Fuse not initialized yet'); 65 return; 66 } 67 68 //console.log('Searching for phrase:', phrase); 69 const results = fuse.search(phrase, { limit: 10 }); 70 //console.log('Search results:', results); 71 displayResults(results, phrase, cursorPos); 72 }, 300); 73 }); 74 75 function displayResults(results, phrase, cursorPos) { 76 if (resultsDiv) { 77 resultsDiv.remove(); 78 } 79 if (results.length === 0) { 80 //console.log('No results for:', phrase); 81 return; 82 } 83 84 resultsDiv = document.createElement('div'); 85 resultsDiv.id = 'fuzzysearch-editor-results'; 86 resultsDiv.style.position = 'absolute'; 87 resultsDiv.style.background = 'white'; 88 resultsDiv.style.border = '1px solid #ccc'; 89 resultsDiv.style.padding = '5px'; 90 resultsDiv.style.zIndex = '1000'; 91 resultsDiv.style.maxHeight = '200px'; 92 resultsDiv.style.overflowY = 'auto'; 93 94 // Get textarea dimensions and position 95 const textareaRect = textarea.getBoundingClientRect(); 96 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 97 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 98 const coords = getCaretCoordinates(textarea, cursorPos); 99 100 // Position the dropdown at the cursor's horizontal level 101 const cursorTop = textareaRect.top + scrollTop + coords.top; 102 const cursorLeft = textareaRect.left + scrollLeft + coords.left; 103 104 // Set initial position 105 resultsDiv.style.left = cursorLeft + 'px'; 106 let desiredTop = cursorTop; // Align with the cursor line 107 108 // Populate the dropdown 109 results.forEach((result, index) => { 110 const page = result.item; 111 const div = document.createElement('div'); 112 div.textContent = page.title; 113 div.dataset.index = index; 114 div.dataset.id = page.id; 115 div.style.cursor = 'pointer'; 116 div.style.padding = '2px 5px'; 117 div.addEventListener('mouseover', () => highlightResult(index)); 118 div.addEventListener('click', () => selectResult(page.id, phrase)); 119 resultsDiv.appendChild(div); 120 }); 121 122 // Append temporarily to measure height 123 document.body.appendChild(resultsDiv); 124 const dropdownHeight = resultsDiv.offsetHeight; 125 const textareaBottom = textareaRect.bottom + scrollTop; 126 127 // Adjust top position to prevent going below textarea bottom 128 if (desiredTop + dropdownHeight > textareaBottom) { 129 desiredTop = textareaBottom - dropdownHeight; 130 } 131 132 // Ensure it doesn't go below the viewport bottom 133 const viewportHeight = window.innerHeight; 134 const maxTop = scrollTop + viewportHeight - dropdownHeight; 135 if (desiredTop > maxTop) { 136 desiredTop = maxTop; 137 } 138 139 // Ensure it doesn't go above the textarea top 140 const textareaTop = textareaRect.top + scrollTop; 141 if (desiredTop < textareaTop) { 142 desiredTop = textareaTop; 143 } 144 145 resultsDiv.style.top = desiredTop + 'px'; 146 147 // Set the first item as selected by default 148 currentIndex = 0; 149 highlightResult(currentIndex); // Highlight the first result 150 151 //console.log('Results displayed at:', resultsDiv.style.top, resultsDiv.style.left); 152 } 153 154 function highlightResult(index) { 155 if (!resultsDiv) return; 156 const items = resultsDiv.children; 157 for (let i = 0; i < items.length; i++) { 158 items[i].style.background = i === index ? '#ddd' : 'white'; 159 } 160 currentIndex = index; 161 if (items[index]) { 162 items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); 163 } 164 } 165 166 function selectResult(pageId, phrase) { 167 const text = textarea.value; 168 const newLink = `[[${pageId}|${phrase}]]`; 169 const start = text.lastIndexOf(`[[${phrase}]]`); 170 textarea.value = text.substring(0, start) + newLink + text.substring(start + phrase.length + 4); 171 if (resultsDiv) { 172 resultsDiv.remove(); 173 resultsDiv = null; 174 } 175 lastPhrase = ''; 176 textarea.focus(); 177 } 178 179 textarea.addEventListener('keydown', function (e) { 180 if (!resultsDiv || resultsDiv.children.length === 0) return; 181 182 if (e.key === 'ArrowDown') { 183 e.preventDefault(); 184 if (currentIndex < resultsDiv.children.length - 1) { 185 currentIndex++; 186 highlightResult(currentIndex); 187 } 188 } else if (e.key === 'ArrowUp') { 189 e.preventDefault(); 190 if (currentIndex > 0) { 191 currentIndex--; 192 highlightResult(currentIndex); 193 } 194 } else if (e.key === 'Space' || e.key === 'Enter') { 195 e.preventDefault(); 196 //console.log('Space or Enter pressed, currentIndex:', currentIndex); // Debug log 197 if (currentIndex >= 0) { 198 const selected = resultsDiv.children[currentIndex]; 199 selectResult(selected.dataset.id, lastPhrase); 200 } 201 } else if (e.key === 'Escape') { 202 if (resultsDiv) { 203 resultsDiv.remove(); 204 resultsDiv = null; 205 lastPhrase = ''; 206 } 207 } 208 }); 209 210 document.addEventListener('click', function (e) { 211 if (resultsDiv && !resultsDiv.contains(e.target) && e.target !== textarea) { 212 resultsDiv.remove(); 213 resultsDiv = null; 214 lastPhrase = ''; 215 } 216 }); 217 218 function getCaretCoordinates(element, position) { 219 const text = element.value.substring(0, position); 220 const lines = text.split('\n'); 221 const lastLine = lines[lines.length - 1]; 222 const font = window.getComputedStyle(element).font; 223 const canvas = document.createElement('canvas'); 224 const context = canvas.getContext('2d'); 225 context.font = font; 226 const width = context.measureText(lastLine).width; 227 const lineHeight = parseInt(font); 228 const top = (lines.length - 1) * lineHeight; 229 return { top: top, left: width }; 230 } 231});