feat: Emoji-Picker Button im Chat-Input (rechts neben Senden)

- #emojiBtn links neben #send platziert
- Fluent Emoji 3D Rendering via parseFluentEmojis()
- Kein Tooltip auf den Emoji-Buttons
- Picker öffnet sich rechts-ausgerichtet über dem Button
- emoji.userdata.js geladen (Kategorien + Mappings)
- Schließt beim Klick außerhalb oder beim destroy()
This commit is contained in:
Meleki committed 2026-03-03 16:18:00 +01:00
1 parent cdd3414771
commit 2673aecc5b
4 files changed
+163

No files matched your search

+51
View File
@@ -318,6 +318,57 @@
color: var(--dangerous);
}
/* Emoji Picker */
.emoji-picker {
position: fixed;
z-index: 500;
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
width: 280px;
max-height: 320px;
overflow-y: auto;
padding: var(--badge-spacing);
display: flex;
flex-direction: column;
gap: var(--box-spacing);
box-shadow: 0 4px 16px rgb(0 0 0 / 0.2);
}
.emoji-picker-section {
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.emoji-picker-label {
font-size: var(--font-size);
opacity: var(--semi-transparent);
padding: 0 .2em;
}
.emoji-picker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(2.6em, 1fr));
}
.emoji-picker-btn {
background: none;
border: none;
padding: .2em .25em;
font-size: 1.3rem;
line-height: 1;
cursor: pointer;
border-radius: var(--border-radius);
transition: background var(--transition-duration);
}
.emoji-picker-btn:hover {
background: var(--f3-bg);
border: none;
}
.stop-btn:hover {
background: rgb(from var(--dangerous) r g b / 0.15);
}
+2
View File
@@ -217,6 +217,7 @@
<button id="fileBtn" class="icon-btn" title="Datei anhängen"><i class="fa fa-paperclip"></i></button>
<button id="explorerBtn" class="icon-btn" title="Workspace durchsuchen"><i class="fa fa-folder"></i></button>
<textarea id="input" placeholder="Nachricht..." rows="1"></textarea>
<button id="emojiBtn" class="icon-btn" title="Emoji einfügen"><i class="fa fa-smile-o"></i></button>
<button id="send" class="icon-btn"><i class="fa fa-paper-plane"></i></button>
<button id="stop" class="icon-btn stop-btn" hidden title="Antwort abbrechen"><i class="fa fa-stop"></i></button>
</div>
@@ -389,6 +390,7 @@
<script src="js/lib/easymde.min.js"></script>
<script src="js/tooltip.js"></script>
<script src="js/emoji.js"></script>
<script src="js/emoji.userdata.js"></script>
<script src="js/ui.js"></script>
<script src="js/api.js"></script>
<script src="js/session.js"></script>
+9
View File
@@ -0,0 +1,9 @@
const EMOJI_USERDATA = {
'Smileys': ['😊','😂','🤣','😄','😁','😅','😆','🙂','😉','😎','🤓','😇','🥳','🤩','😍','🥰','😘','😋','🤔','🤨','😐','😑','🙄','😏','😬','🤫','🤭','😌','😴','🥱','😮','😲','🤯','😳','😱','😨','😰','😢','😭','😤','😡','🤬','💀','😈','👿','🤡','💩','👻','👽','🤖'],
'Gesten': ['👍','👎','👌','✌️','🤞','🤙','👋','🤚','✋','🖐️','👆','☝️','👇','👈','👉','👊','✊','🤛','🤜','🙏','👏','🙌','🤝','💪','🤟','🤲'],
'Herzen': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','💕','💖','💗','💘','💝'],
'Objekte': ['🔥','⭐','✨','💡','💎','🎯','🎮','🎵','🎶','📌','📎','✏️','📝','📚','💻','⌨️','🖥️','📱','🔧','🔨','⚡','🚀','🏆','🎉','🎊','🎁','🎈'],
'Natur': ['🌞','🌙','⛅','🌈','🌊','🍀','🌸','🌺','🌻','🌲','🍂','❄️','🐱','🐶','🦊','🐻','🐼','🐨','🦁','🐸','🐧','🦄','🐝','🦋'],
'Essen': ['☕','🍺','🍷','🍕','🍔','🍟','🌭','🍿','🧁','🍰','🍩','🍪','🍫','🍉','🍓','🍑','🥑','🌶️'],
'Reaktionen': ['✅','❌','⚠️','❓','❗','💯','♻️','🔄','⏳','⏰','🔔','🔕','📢','💬','💭','👀','🫠','🤷','🤦','🥺',]
};
+101
View File
@@ -2,6 +2,97 @@
let _streamingPollInterval = null;
let _recoveryChainId = null;
let _emojiPickerEl = null;
let _emojiPickerCloseHandler = null;
function _buildEmojiPicker() {
const picker = document.createElement('div');
picker.className = 'emoji-picker';
for (const [category, emojis] of Object.entries(EMOJI_USERDATA)) {
const section = document.createElement('div');
section.className = 'emoji-picker-section';
const label = document.createElement('div');
label.className = 'emoji-picker-label';
label.textContent = category;
section.appendChild(label);
const grid = document.createElement('div');
grid.className = 'emoji-picker-grid';
for (const emoji of emojis) {
const btn = document.createElement('button');
btn.className = 'emoji-picker-btn';
btn.textContent = emoji;
btn.addEventListener('click', (e) => {
e.stopPropagation();
const input = document.getElementById('input');
if (input) {
const start = input.selectionStart;
const end = input.selectionEnd;
const val = input.value;
input.value = val.slice(0, start) + emoji + val.slice(end);
const pos = start + emoji.length;
input.setSelectionRange(pos, pos);
input.dispatchEvent(new Event('input'));
input.focus();
}
closeEmojiPicker();
});
// Fluent Emoji im Button rendern
parseFluentEmojis(btn);
grid.appendChild(btn);
}
section.appendChild(grid);
picker.appendChild(section);
}
return picker;
}
function toggleEmojiPicker(anchor) {
if (_emojiPickerEl) {
closeEmojiPicker();
return;
}
const picker = _buildEmojiPicker();
document.body.appendChild(picker);
_emojiPickerEl = picker;
// Positionierung: über dem Button, rechts ausgerichtet
const rect = anchor.getBoundingClientRect();
const pickerHeight = 320;
const pickerWidth = 280;
let top = rect.top - pickerHeight - 6;
let left = rect.right - pickerWidth;
// Falls kein Platz oben → unter dem Button
if (top < 8) top = rect.bottom + 6;
// Falls links abgeschnitten → linksbündig zum Button
if (left < 8) left = 8;
picker.style.top = top + 'px';
picker.style.left = left + 'px';
_emojiPickerCloseHandler = (e) => {
if (!picker.contains(e.target)) closeEmojiPicker();
};
setTimeout(() => document.addEventListener('click', _emojiPickerCloseHandler), 0);
}
function closeEmojiPicker() {
if (_emojiPickerEl) {
_emojiPickerEl.remove();
_emojiPickerEl = null;
}
if (_emojiPickerCloseHandler) {
document.removeEventListener('click', _emojiPickerCloseHandler);
_emojiPickerCloseHandler = null;
}
}
function startStreamingPoll(sessionId, sendBtn, chainId) {
_recoveryChainId = chainId;
@@ -190,6 +281,15 @@ registerPage('chat', {
inputEl.dispatchEvent(new Event('input'))
}
// Emoji Picker
const emojiBtn = document.getElementById('emojiBtn');
if (emojiBtn) {
emojiBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleEmojiPicker(emojiBtn);
});
}
// Workspace Explorer
const explorerBtn = document.getElementById('explorerBtn');
if (explorerBtn) {
@@ -275,6 +375,7 @@ registerPage('chat', {
// Stream nicht abbrechen (Backend soll weiterlaufen),
// aber Frontend-Processing stoppen
_chatAbandoned = true;
closeEmojiPicker();
if (this._closeMenu) {
document.removeEventListener('click', this._closeMenu);
this._closeMenu = null;