first version of autocomplete. see #157

This commit is contained in:
mic 2024-10-13 23:31:11 +02:00
parent ed596041ed
commit 4230318b10
3 changed files with 160 additions and 4 deletions

View file

@ -163,6 +163,44 @@ table .sort.desc {
margin-bottom: 0px;
}
.autocomplete-container {
position: relative;
}
.autocomplete-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ccc;
z-index: 1000;
max-height: 200px;
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
font-size: small;
}
.autocomplete-list li {
padding: 8px;
cursor: pointer;
}
.autocomplete-list li:hover {
background-color: #f0f0f0;
}
.autocomplete-list li.active {
background-color: #ddd;
}
.hidden {
display: none;
}
@media (prefers-color-scheme: dark) {
body {
@ -196,6 +234,18 @@ table .sort.desc {
.storage_space{
color: rgb(182, 182, 182);
}
}
.autocomplete-list {
background-color: #2E2F36;
border: 1px solid #2E2F36;
}
.autocomplete-list li:hover {
background-color: #4c4e58;
}
.autocomplete-list li.active {
background-color: #4c4e58;
}
}

View file

@ -34,7 +34,10 @@
<td rowspan="2" class="w25">
<label for="txtTextNew">*__MSG_customPrompts_form_label_Text__:</label>
<br>
<textarea id="txtTextNew" name="text" class="input_new text_output" tabindex="3"></textarea>
<div class="autocomplete-container">
<textarea id="txtTextNew" name="text" class="input_new text_output editor" tabindex="3"></textarea>
<ul class="autocomplete-list hidden"></ul>
</div>
</td>
<td class="w19">
<label for="selectTypeNew">__MSG_customPrompts_add_to_menu__:</label>

View file

@ -19,6 +19,7 @@
import { getPrompts, setDefaultPromptsProperties, setCustomPrompts, preparePromptsForExport, preparePromptsForImport } from "../js/mzta-prompts.js";
import { isThunderbird128OrGreater, getCustomPromptsUsedSpace } from "../js/mzta-utils.js";
import { taLogger } from "../js/mzta-logger.js";
import { getPlaceholders } from "../js/mzta-placeholders.js";
var promptsList = null;
var somethingChanged = false;
@ -27,6 +28,7 @@ var positionMax_display = 0;
var idnumMax = 0;
var msgTimeout = null;
let taLog = null;
let autocompleteSuggestions = [];
document.addEventListener('DOMContentLoaded', async () => {
@ -111,6 +113,13 @@ document.addEventListener('DOMContentLoaded', async () => {
});
}
const textareas = document.querySelectorAll('.editor');
autocompleteSuggestions = (await getPlaceholders(true)).map(p => '{%'+p.id+'%}');
// console.log('>>>>>>>>>>> suggestions: ' + JSON.stringify(suggestions));
textareas.forEach(textarea => textareaAutocomplete(textarea, autocompleteSuggestions));
i18n.updateDocument();
//To add a new item
@ -274,7 +283,9 @@ function showItemRowEditor(tr) {
tr.querySelector('.id_show').style.display = 'none';
tr.querySelector('.name_output').style.display = 'inline';
tr.querySelector('.name_show').style.display = 'none';
tr.querySelector('.text_output').style.display = 'inline';
const text_output = tr.querySelector('.text_output');
text_output.style.display = 'inline';
textareaAutocomplete(text_output, autocompleteSuggestions)
tr.querySelector('.text_show').style.display = 'none';
tr.querySelector('.type_output').style.display = 'inline';
tr.querySelector('.type_show').style.display = 'none';
@ -399,7 +410,7 @@ function loadPromptsList(values){
let output = `<tr ` + ((values.is_default == 1) ? 'class="is_default"':'') + `>
<td class="w08"><span class="id id_show"></span><input type="text" class="hiddendata id_output" value="` + values.id + `" /></td>
<td class="w08"><span class="name name_show"></span><input type="text" class="hiddendata name_output" value="` + values.name + `" /></td>
<td class="w40"><span class="text text_show"></span><textarea class="hiddendata text_output">` + values.text + `</textarea></td>
<td class="w40"><span class="text text_show"></span><div class="autocomplete-container"><textarea class="hiddendata text_output editor">` + values.text + `</textarea><ul class="autocomplete-list hidden"></ul></div></td>
<td class="w08"><span class="type_show">` + type_output + `</span>
<select class="type_output hiddendata">
<option value="0"` + ((values.type == "0") ? ' selected':'') + `>__MSG_customPrompts_add_to_menu_always__</option>
@ -642,3 +653,95 @@ if(await isThunderbird128OrGreater()){
}
});
}
function textareaAutocomplete(textarea, suggestions) {
const container = textarea.closest('.autocomplete-container');
const autocompleteList = container.querySelector('.autocomplete-list');
let activeIndex = -1;
textarea.addEventListener('input', () => {
const cursorPosition = textarea.selectionStart;
const text = textarea.value.substring(0, cursorPosition);
const match = text.match(/{%[^\s]*$/);
if (match) {
const lastWord = match[0];
const matches = suggestions.filter(s => s.startsWith(lastWord));
showSuggestions(matches, autocompleteList);
} else {
hideSuggestions(autocompleteList);
}
});
textarea.addEventListener('keydown', (e) => {
const items = autocompleteList.querySelectorAll('li');
if (items.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIndex = (activeIndex + 1) % items.length;
updateActiveSuggestion(items, activeIndex);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIndex = (activeIndex - 1 + items.length) % items.length;
updateActiveSuggestion(items, activeIndex);
} else if (e.key === 'Enter') {
e.preventDefault();
if (activeIndex >= 0 && activeIndex < items.length) {
insertAutocomplete(items[activeIndex].textContent, textarea);
hideSuggestions(autocompleteList);
}
} else if (e.key === 'Escape') {
hideSuggestions(autocompleteList);
}
}
});
function showSuggestions(matches, autocompleteList) {
autocompleteList.innerHTML = '';
activeIndex = -1;
if (matches.length === 0) {
hideSuggestions(autocompleteList);
return;
}
matches.forEach(match => {
const li = document.createElement('li');
li.textContent = match;
li.addEventListener('click', () => {
insertAutocomplete(match, textarea);
hideSuggestions(autocompleteList);
});
autocompleteList.appendChild(li);
});
autocompleteList.classList.remove('hidden');
}
function hideSuggestions(autocompleteList) {
autocompleteList.classList.add('hidden');
activeIndex = -1;
}
function updateActiveSuggestion(items, activeIndex) {
items.forEach((item, index) => {
item.classList.remove('active');
if (index === activeIndex) {
item.classList.add('active');
item.scrollIntoView({ block: 'nearest' });
}
});
}
function insertAutocomplete(suggestion, textarea) {
const cursorPosition = textarea.selectionStart;
const textBefore = textarea.value.substring(0, cursorPosition);
const textAfter = textarea.value.substring(cursorPosition);
const match = textBefore.match(/{%[^\s]*$/);
if (match) {
const lastWord = match[0];
const completion = suggestion.substring(lastWord.length);
const newText = textBefore + completion + textAfter;
textarea.value = newText;
const newCursorPosition = cursorPosition + completion.length;
textarea.setSelectionRange(newCursorPosition, newCursorPosition);
}
}
}