xref: /plugin/fuzzysearch/editor.js (revision 66fcc8eae76a77d88286dc2434b78514d875fb8c)
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});