mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
* 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
180 lines
4.4 KiB
JavaScript
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
|
|
}
|
|
}
|