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:
1 parent
cdd3414771
commit
2673aecc5b
4 files changed
+163
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const EMOJI_USERDATA = {
|
||||
'Smileys': ['😊','😂','🤣','😄','😁','😅','😆','🙂','😉','😎','🤓','😇','🥳','🤩','😍','🥰','😘','😋','🤔','🤨','😐','😑','🙄','😏','😬','🤫','🤭','😌','😴','🥱','😮','😲','🤯','😳','😱','😨','😰','😢','😭','😤','😡','🤬','💀','😈','👿','🤡','💩','👻','👽','🤖'],
|
||||
'Gesten': ['👍','👎','👌','✌️','🤞','🤙','👋','🤚','✋','🖐️','👆','☝️','👇','👈','👉','👊','✊','🤛','🤜','🙏','👏','🙌','🤝','💪','🤟','🤲'],
|
||||
'Herzen': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','💕','💖','💗','💘','💝'],
|
||||
'Objekte': ['🔥','⭐','✨','💡','💎','🎯','🎮','🎵','🎶','📌','📎','✏️','📝','📚','💻','⌨️','🖥️','📱','🔧','🔨','⚡','🚀','🏆','🎉','🎊','🎁','🎈'],
|
||||
'Natur': ['🌞','🌙','⛅','🌈','🌊','🍀','🌸','🌺','🌻','🌲','🍂','❄️','🐱','🐶','🦊','🐻','🐼','🐨','🦁','🐸','🐧','🦄','🐝','🦋'],
|
||||
'Essen': ['☕','🍺','🍷','🍕','🍔','🍟','🌭','🍿','🧁','🍰','🍩','🍪','🍫','🍉','🍓','🍑','🥑','🌶️'],
|
||||
'Reaktionen': ['✅','❌','⚠️','❓','❗','💯','♻️','🔄','⏳','⏰','🔔','🔕','📢','💬','💭','👀','🫠','🤷','🤦','🥺',]
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user