-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathautocomplete.js
88 lines (83 loc) · 4.55 KB
/
autocomplete.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions.
* Files: autocomplete.js / autocomplete.css
*/
codeInput.plugins.Autocomplete = class extends codeInput.Plugin {
/**
* Pass in a function to create a plugin that displays the popup that takes in (popup element, textarea, textarea.selectionEnd).
* @param {(popupElement: HTMLElement, textarea: HTMLTextAreaElement, selectionEnd: number) => void} updatePopupCallback a function to display the popup that takes in (popup element, textarea, textarea.selectionEnd).
*/
constructor(updatePopupCallback) {
super([]); // No observed attributes
this.updatePopupCallback = updatePopupCallback;
}
/* When a key is pressed, or scrolling occurs, update the popup position */
updatePopup(codeInput, onlyScrolled) {
let textarea = codeInput.textareaElement;
let caretCoords = this.getCaretCoordinates(codeInput, textarea, textarea.selectionEnd, onlyScrolled);
let popupElem = codeInput.querySelector(".code-input_autocomplete_popup");
popupElem.style.top = caretCoords.top + "px";
popupElem.style.left = caretCoords.left + "px";
if(!onlyScrolled) {
this.updatePopupCallback(popupElem, textarea, textarea.selectionEnd);
}
}
/* Create the popup element */
afterElementsAdded(codeInput) {
let popupElem = document.createElement("div");
popupElem.classList.add("code-input_autocomplete_popup");
codeInput.appendChild(popupElem);
let testPosPre = document.createElement("pre");
popupElem.setAttribute("inert", true); // Invisible to keyboard navigation
popupElem.setAttribute("tabindex", -1); // Invisible to keyboard navigation
testPosPre.setAttribute("aria-hidden", true); // Hide for screen readers
if(codeInput.template.preElementStyled) {
testPosPre.classList.add("code-input_autocomplete_testpos");
codeInput.appendChild(testPosPre); // Styled like first pre, but first pre found to update
} else {
let testPosCode = document.createElement("code");
testPosCode.classList.add("code-input_autocomplete_testpos");
testPosPre.appendChild(testPosCode);
codeInput.appendChild(testPosPre); // Styled like first pre, but first pre found to update
}
let textarea = codeInput.textareaElement;
textarea.addEventListener("input", () => { this.updatePopup(codeInput, false)});
textarea.addEventListener("click", () => { this.updatePopup(codeInput, false)});
}
/**
* Return the coordinates of the caret in a code-input
* @param {codeInput.CodeInput} codeInput
* @param {HTMLElement} textarea
* @param {Number} charIndex
* @param {boolean} onlyScrolled True if no edits have been made to the text and the caret hasn't been repositioned
* @returns {Object} {"top": CSS top value in pixels, "left": CSS left value in pixels}
*/
getCaretCoordinates(codeInput, textarea, charIndex, onlyScrolled) {
let afterSpan;
if(onlyScrolled) {
// No edits to text; don't update element - span at index 1 is after span
let spans = codeInput.querySelector(".code-input_autocomplete_testpos").querySelectorAll("span");
if(spans.length < 2) {
// Hasn't saved text in test pre to find pos
// Need to regenerate text in test pre
return this.getCaretCoordinates(codeInput, textarea, charIndex, false);
}
afterSpan = spans[1];
} else {
/* Inspired by https://github.com/component/textarea-caret-position */
let testPosElem = codeInput.querySelector(".code-input_autocomplete_testpos");
let beforeSpan = document.createElement("span");
beforeSpan.textContent = textarea.value.substring(0, charIndex);
afterSpan = document.createElement("span");
afterSpan.textContent = "."; // Placeholder
// Clear test pre - https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
while (testPosElem.firstChild) {
testPosElem.removeChild(testPosElem.firstChild);
}
testPosElem.appendChild(beforeSpan);
testPosElem.appendChild(afterSpan);
}
return {"top": afterSpan.offsetTop - textarea.scrollTop, "left": afterSpan.offsetLeft - textarea.scrollLeft};
}
updatePopupCallback = function() {};
}