document.addEventListener('DOMContentLoaded', function () { // Function to initialize fuzzy search for a given input or textarea element function initializeFuzzySearch(element) { if (!element) { console.error('Element not found!'); return; } let resultsDiv = null; let currentIndex = -1; let lastPhrase = ''; let fuse = null; let pagesCache = null; let searchTimeout = null; // Fetch pages and initialize Fuse.js fetch(DOKU_BASE + 'lib/exe/ajax.php?call=fuzzysearch_pages', { method: 'GET', credentials: 'same-origin' }) .then(response => { if (!response.ok) throw new Error('Failed to fetch pages'); return response.json(); }) .then(pages => { pagesCache = pages; fuse = new Fuse(pagesCache, { keys: ['title'], threshold: 0.4, includeScore: true, maxPatternLength: 32, minMatchCharLength: 2 }); }) .catch(error => console.error('Initial fetch error:', error)); // Function to handle search logic function handleInputChange() { if (searchTimeout) clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const cursorPos = element.selectionStart || element.value.length; const text = element.value; const match = text.substring(0, cursorPos).match(/\[\[([^\[\]]+)\]\]$/); if (!match) { if (resultsDiv) { resultsDiv.remove(); resultsDiv = null; } return; } const phrase = match[1].trim(); if (!phrase || phrase === lastPhrase) { return; } lastPhrase = phrase; if (!fuse) { console.error('Fuse not initialized yet'); return; } const results = fuse.search(phrase, { limit: 10 }); displayResults(results, phrase, cursorPos); }, 300); // Debounce delay } // Add event listeners element.addEventListener('keyup', function (e) { if (e.key === ']') { handleInputChange(); } }); element.addEventListener('input', handleInputChange); element.addEventListener('compositionend', handleInputChange); // Display results function function displayResults(results, phrase, cursorPos) { if (resultsDiv) { resultsDiv.remove(); } if (results.length === 0) { return; } resultsDiv = document.createElement('div'); resultsDiv.id = 'fuzzysearch-editor-results'; resultsDiv.style.position = 'absolute'; resultsDiv.style.background = 'white'; resultsDiv.style.border = '1px solid #ccc'; resultsDiv.style.padding = '5px'; resultsDiv.style.zIndex = '1000'; resultsDiv.style.maxHeight = '200px'; resultsDiv.style.overflowY = 'auto'; const elementRect = element.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; const coords = getCaretCoordinates(element, cursorPos); const cursorTop = elementRect.top + scrollTop + coords.top; const cursorLeft = elementRect.left + scrollLeft + coords.left; resultsDiv.style.left = cursorLeft + 'px'; let desiredTop = cursorTop; results.forEach((result, index) => { const page = result.item; const div = document.createElement('div'); div.textContent = page.title; div.dataset.index = index; div.dataset.id = page.id; div.style.cursor = 'pointer'; div.style.padding = '2px 5px'; div.addEventListener('mouseover', () => highlightResult(index)); div.addEventListener('click', () => selectResult(page.id, phrase)); resultsDiv.appendChild(div); }); document.body.appendChild(resultsDiv); const dropdownHeight = resultsDiv.offsetHeight; const elementBottom = elementRect.bottom + scrollTop; if (desiredTop + dropdownHeight > elementBottom) { desiredTop = elementBottom - dropdownHeight; } const viewportHeight = window.innerHeight; const maxTop = scrollTop + viewportHeight - dropdownHeight; if (desiredTop > maxTop) { desiredTop = maxTop; } const elementTop = elementRect.top + scrollTop; if (desiredTop < elementTop) { desiredTop = elementTop; } resultsDiv.style.top = desiredTop + 'px'; currentIndex = 0; highlightResult(currentIndex); } function highlightResult(index) { if (!resultsDiv) return; const items = resultsDiv.children; for (let i = 0; i < items.length; i++) { items[i].style.background = i === index ? '#ddd' : 'white'; } currentIndex = index; if (items[index]) { items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } } function selectResult(pageId, phrase) { const text = element.value; const newLink = `[[${pageId}|${phrase}]]`; const start = text.lastIndexOf(`[[${phrase}]]`); element.value = text.substring(0, start) + newLink + text.substring(start + phrase.length + 4); if (resultsDiv) { resultsDiv.remove(); resultsDiv = null; } lastPhrase = ''; element.focus(); } element.addEventListener('keydown', function (e) { if (!resultsDiv || resultsDiv.children.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); if (currentIndex < resultsDiv.children.length - 1) { currentIndex++; highlightResult(currentIndex); } } else if (e.key === 'ArrowUp') { e.preventDefault(); if (currentIndex > 0) { currentIndex--; highlightResult(currentIndex); } } else if (e.key === 'Space' || e.key === 'Enter') { e.preventDefault(); if (currentIndex >= 0) { const selected = resultsDiv.children[currentIndex]; selectResult(selected.dataset.id, lastPhrase); } } else if (e.key === 'Escape') { if (resultsDiv) { resultsDiv.remove(); resultsDiv = null; lastPhrase = ''; } } }); document.addEventListener('click', function (e) { if (resultsDiv && !resultsDiv.contains(e.target) && e.target !== element) { resultsDiv.remove(); resultsDiv = null; lastPhrase = ''; } }); function getCaretCoordinates(element, position) { const isTextarea = element.tagName.toLowerCase() === 'textarea'; const text = element.value.substring(0, position); const font = window.getComputedStyle(element).font; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); context.font = font; if (isTextarea) { const lines = text.split('\n'); const lastLine = lines[lines.length - 1]; const width = context.measureText(lastLine).width; const lineHeight = parseInt(font); const top = (lines.length - 1) * lineHeight; return { top: top, left: width }; } else { // For input type="text", no line breaks, just measure the text width const width = context.measureText(text).width; const lineHeight = parseInt(font); return { top: 0, left: width }; } } } // Target the main wiki editor textarea const wikiTextarea = document.querySelector('textarea[name="wikitext"]'); if (wikiTextarea) { initializeFuzzySearch(wikiTextarea); } // Target Bureaucracy form textareas and textboxes const bureaucracyElements = document.querySelectorAll('.bureaucracy__plugin textarea, .bureaucracy__plugin input[type="text"]'); bureaucracyElements.forEach(element => { initializeFuzzySearch(element); }); // Use MutationObserver to catch dynamically added Bureaucracy form elements const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const newElements = mutation.target.querySelectorAll('.bureaucracy__plugin textarea, .bureaucracy__plugin input[type="text"]'); newElements.forEach(element => { if (!element.dataset.fuzzyInitialized) { initializeFuzzySearch(element); element.dataset.fuzzyInitialized = 'true'; // Mark as initialized } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); });