dawarich/app/javascript/controllers/emoji_picker_controller.js
2025-11-19 19:17:30 +01:00

180 lines
4.4 KiB
JavaScript

import { Controller } from "@hotwired/stimulus"
import { Picker } from "emoji-mart"
// Emoji Picker Controller
// Based on RailsBlocks pattern: https://railsblocks.com/docs/emoji-picker
export default class extends Controller {
static targets = ["input", "button", "pickerContainer"]
static values = {
autoSubmit: { type: Boolean, default: true }
}
connect() {
this.picker = null
this.setupKeyboardListeners()
}
disconnect() {
this.removePicker()
this.removeKeyboardListeners()
}
toggle(event) {
event.preventDefault()
event.stopPropagation()
if (this.pickerContainerTarget.classList.contains("hidden")) {
this.open()
} else {
this.close()
}
}
open() {
if (!this.picker) {
this.createPicker()
}
this.pickerContainerTarget.classList.remove("hidden")
this.setupOutsideClickListener()
}
close() {
this.pickerContainerTarget.classList.add("hidden")
this.removeOutsideClickListener()
}
createPicker() {
this.picker = new Picker({
onEmojiSelect: this.onEmojiSelect.bind(this),
theme: this.getTheme(),
previewPosition: "none",
skinTonePosition: "search",
maxFrequentRows: 2,
perLine: 8,
navPosition: "bottom",
categories: [
"frequent",
"people",
"nature",
"foods",
"activity",
"places",
"objects",
"symbols",
"flags"
]
})
this.pickerContainerTarget.appendChild(this.picker)
}
onEmojiSelect(emoji) {
if (!emoji || !emoji.native) return
// Update input value
this.inputTarget.value = emoji.native
// Update button to show selected emoji
if (this.hasButtonTarget) {
// Find the display element (could be a span or the button itself)
const display = this.buttonTarget.querySelector('[data-emoji-picker-display]') || this.buttonTarget
display.textContent = emoji.native
}
// Close picker
this.close()
// Auto-submit if enabled
if (this.autoSubmitValue) {
this.submitForm()
}
// Dispatch custom event for advanced use cases
this.dispatch("select", { detail: { emoji: emoji.native } })
}
submitForm() {
const form = this.element.closest("form")
if (form && !form.requestSubmit) {
// Fallback for older browsers
form.submit()
} else if (form) {
form.requestSubmit()
}
}
clearEmoji(event) {
event?.preventDefault()
this.inputTarget.value = ""
if (this.hasButtonTarget) {
const display = this.buttonTarget.querySelector('[data-emoji-picker-display]') || this.buttonTarget
// Reset to default emoji or icon
const defaultIcon = this.buttonTarget.dataset.defaultIcon || "😀"
display.textContent = defaultIcon
}
this.dispatch("clear")
}
getTheme() {
// Detect dark mode from document
if (document.documentElement.getAttribute('data-theme') === 'dark' ||
document.documentElement.classList.contains('dark')) {
return 'dark'
}
return 'light'
}
setupKeyboardListeners() {
this.handleKeydown = this.handleKeydown.bind(this)
document.addEventListener("keydown", this.handleKeydown)
}
removeKeyboardListeners() {
document.removeEventListener("keydown", this.handleKeydown)
}
handleKeydown(event) {
// Close on Escape
if (event.key === "Escape" && !this.pickerContainerTarget.classList.contains("hidden")) {
this.close()
}
// Clear on Delete/Backspace (when picker is open)
if ((event.key === "Delete" || event.key === "Backspace") &&
!this.pickerContainerTarget.classList.contains("hidden") &&
event.target === this.inputTarget) {
event.preventDefault()
this.clearEmoji()
}
}
setupOutsideClickListener() {
this.handleOutsideClick = this.handleOutsideClick.bind(this)
// Use setTimeout to avoid immediate triggering from the toggle click
setTimeout(() => {
document.addEventListener("click", this.handleOutsideClick)
}, 0)
}
removeOutsideClickListener() {
if (this.handleOutsideClick) {
document.removeEventListener("click", this.handleOutsideClick)
}
}
handleOutsideClick(event) {
if (!this.element.contains(event.target)) {
this.close()
}
}
removePicker() {
if (this.picker && this.picker.remove) {
this.picker.remove()
}
this.picker = null
}
}