refactor(admin): migrate dropdowns to clickOutside action
Updated components to use the new clickOutside action instead of manual event listener management: - ProjectListItem: Add clickOutside action and dropdown coordination - AdminSegmentedController: Replace $effect with clickOutside action - BubbleTextStyleMenu: Simplify click-outside handling - BubbleColorPicker: Simplify click-outside handling - Posts/Projects pages: Remove redundant page-level click handlers The clickOutside action provides a cleaner, more maintainable way to handle click-outside behavior with proper lifecycle management. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
97a80d9c3e
commit
9cc7baddc6
6 changed files with 27 additions and 83 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import BaseSegmentedController from './BaseSegmentedController.svelte'
|
||||
import { clickOutside } from '$lib/actions/clickOutside'
|
||||
|
||||
const currentPath = $derived($page.url.pathname)
|
||||
|
||||
|
|
@ -34,20 +35,9 @@
|
|||
: ''
|
||||
)
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
$effect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.closest('.dropdown-container')) {
|
||||
function handleClickOutside() {
|
||||
showDropdown = false
|
||||
}
|
||||
}
|
||||
|
||||
if (showDropdown) {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
return () => document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<nav class="admin-segmented-controller">
|
||||
|
|
@ -66,7 +56,11 @@
|
|||
{/snippet}
|
||||
</BaseSegmentedController>
|
||||
|
||||
<div class="dropdown-container">
|
||||
<div
|
||||
class="dropdown-container"
|
||||
use:clickOutside={{ enabled: showDropdown }}
|
||||
on:clickoutside={handleClickOutside}
|
||||
>
|
||||
<button
|
||||
class="dropdown-trigger"
|
||||
onclick={() => (showDropdown = !showDropdown)}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { goto } from '$app/navigation'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import AdminByline from './AdminByline.svelte'
|
||||
import { clickOutside } from '$lib/actions/clickOutside'
|
||||
|
||||
import type { AdminProject } from '$lib/types/admin'
|
||||
|
||||
|
|
@ -48,6 +49,10 @@
|
|||
|
||||
function handleToggleDropdown(event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
// Close all other dropdowns before toggling this one
|
||||
if (!isDropdownOpen) {
|
||||
document.dispatchEvent(new CustomEvent('closeDropdowns'))
|
||||
}
|
||||
isDropdownOpen = !isDropdownOpen
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +68,10 @@
|
|||
dispatch('delete', { project })
|
||||
}
|
||||
|
||||
function handleClickOutside() {
|
||||
isDropdownOpen = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function handleCloseDropdowns() {
|
||||
isDropdownOpen = false
|
||||
|
|
@ -99,7 +108,11 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-container">
|
||||
<div
|
||||
class="dropdown-container"
|
||||
use:clickOutside={{ enabled: isDropdownOpen }}
|
||||
on:clickoutside={handleClickOutside}
|
||||
>
|
||||
<button
|
||||
class="action-button"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'
|
||||
import { clickOutside } from '$lib/actions/clickOutside'
|
||||
|
||||
interface Props {
|
||||
editor: Editor
|
||||
|
|
@ -97,30 +98,10 @@
|
|||
applyColor(color)
|
||||
onClose()
|
||||
}
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (isOpen) {
|
||||
// Check if click is inside the color picker popup
|
||||
const pickerElement = document.querySelector('.bubble-color-picker')
|
||||
if (pickerElement && !pickerElement.contains(e.target as Node)) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Add a small delay to prevent immediate closing
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}, 10)
|
||||
return () => document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="bubble-color-picker">
|
||||
<div class="bubble-color-picker" use:clickOutside on:clickoutside={onClose}>
|
||||
<div class="color-picker-header">
|
||||
<span>{mode === 'text' ? 'Text Color' : 'Highlight Color'}</span>
|
||||
<button class="remove-color-btn" onclick={removeColor}> Remove </button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { clickOutside } from '$lib/actions/clickOutside'
|
||||
|
||||
interface Props {
|
||||
editor: Editor
|
||||
|
|
@ -67,29 +68,10 @@
|
|||
action()
|
||||
onClose()
|
||||
}
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (isOpen) {
|
||||
const menu = e.currentTarget as HTMLElement
|
||||
if (!menu?.contains(e.target as Node)) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Small delay to prevent immediate closing
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}, 10)
|
||||
return () => document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="bubble-text-style-menu">
|
||||
<div class="bubble-text-style-menu" use:clickOutside on:clickoutside={onClose}>
|
||||
{#each textStyles as style}
|
||||
<button
|
||||
class="text-style-option"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { goto, invalidate } from '$app/navigation'
|
||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||
|
|
@ -68,18 +67,6 @@ const statusFilterOptions = [
|
|||
{ value: 'status-draft', label: 'Draft first' }
|
||||
]
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
return () => document.removeEventListener('click', handleOutsideClick)
|
||||
})
|
||||
|
||||
function handleOutsideClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.dropdown-container')) {
|
||||
document.dispatchEvent(new CustomEvent('closeDropdowns'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleNewEssay() {
|
||||
goto('/admin/posts/new?type=essay')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||
|
|
@ -70,18 +69,6 @@
|
|||
{ value: 'status-draft', label: 'Draft first' }
|
||||
]
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
return () => document.removeEventListener('click', handleOutsideClick)
|
||||
})
|
||||
|
||||
function handleOutsideClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest('.dropdown-container')) {
|
||||
document.dispatchEvent(new CustomEvent('closeDropdowns'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit(event: CustomEvent<{ project: AdminProject }>) {
|
||||
goto(`/admin/projects/${event.detail.project.id}/edit`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue