dawarich/app/javascript/controllers/emoji_picker_controller.js
Evgenii Burmakin b1393ee674
0.36.0 (#1952)
* Implement OmniAuth GitHub authentication

* Fix omniauth GitHub scope to include user email access

* Remove margin-bottom

* Implement Google OAuth2 authentication

* Implement OIDC authentication for Dawarich using omniauth_openid_connect gem.

* Add patreon account linking and patron checking service

* Update docker-compose.yml to use boolean values instead of strings

* Add support for KML files

* Add tests

* Update changelog

* Remove patreon OAuth integration

* Move omniauthable to a concern

* Update an icon in integrations

* Update changelog

* Update app version

* Fix family location sharing toggle

* Move family location sharing to its own controller

* Update changelog

* Implement basic tagging functionality for places, allowing users to categorize and label places with custom tags.

* Add places management API and tags feature

* Add some changes related to places management feature

* Fix some tests

* Fix sometests

* Add places layer

* Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control

* Rework tag form

* Add hashtag

* Add privacy zones to tags

* Add notes to places and manage place tags

* Update changelog

* Update e2e tests

* Extract tag serializer to its own file

* Fix some tests

* Fix tags request specs

* Fix some tests

* Fix rest of the tests

* Revert some changes

* Add missing specs

* Revert changes in place export/import code

* Fix some specs

* Fix PlaceFinder to only consider global places when finding existing places

* Fix few more specs

* Fix visits creator spec

* Fix last tests

* Update place creating modal

* Add home location based on "Home" tagged place

* Save enabled tag layers

* Some fixes

* Fix bug where enabling place tag layers would trigger saving enabled layers, overwriting with incomplete data

* Update migration to use disable_ddl_transaction! and add up/down methods

* Fix tag layers restoration and filtering logic

* Update OIDC auto-registration and email/password registration settings

* Fix potential xss
2025-11-24 19:45:09 +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
}
}