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">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import BaseSegmentedController from './BaseSegmentedController.svelte'
|
import BaseSegmentedController from './BaseSegmentedController.svelte'
|
||||||
|
import { clickOutside } from '$lib/actions/clickOutside'
|
||||||
|
|
||||||
const currentPath = $derived($page.url.pathname)
|
const currentPath = $derived($page.url.pathname)
|
||||||
|
|
||||||
|
|
@ -34,20 +35,9 @@
|
||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
function handleClickOutside() {
|
||||||
$effect(() => {
|
showDropdown = false
|
||||||
function handleClickOutside(e: MouseEvent) {
|
}
|
||||||
const target = e.target as HTMLElement
|
|
||||||
if (!target.closest('.dropdown-container')) {
|
|
||||||
showDropdown = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDropdown) {
|
|
||||||
document.addEventListener('click', handleClickOutside)
|
|
||||||
return () => document.removeEventListener('click', handleClickOutside)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="admin-segmented-controller">
|
<nav class="admin-segmented-controller">
|
||||||
|
|
@ -66,7 +56,11 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</BaseSegmentedController>
|
</BaseSegmentedController>
|
||||||
|
|
||||||
<div class="dropdown-container">
|
<div
|
||||||
|
class="dropdown-container"
|
||||||
|
use:clickOutside={{ enabled: showDropdown }}
|
||||||
|
on:clickoutside={handleClickOutside}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="dropdown-trigger"
|
class="dropdown-trigger"
|
||||||
onclick={() => (showDropdown = !showDropdown)}
|
onclick={() => (showDropdown = !showDropdown)}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { createEventDispatcher, onMount } from 'svelte'
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
import AdminByline from './AdminByline.svelte'
|
import AdminByline from './AdminByline.svelte'
|
||||||
|
import { clickOutside } from '$lib/actions/clickOutside'
|
||||||
|
|
||||||
import type { AdminProject } from '$lib/types/admin'
|
import type { AdminProject } from '$lib/types/admin'
|
||||||
|
|
||||||
|
|
@ -48,6 +49,10 @@
|
||||||
|
|
||||||
function handleToggleDropdown(event: MouseEvent) {
|
function handleToggleDropdown(event: MouseEvent) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
// Close all other dropdowns before toggling this one
|
||||||
|
if (!isDropdownOpen) {
|
||||||
|
document.dispatchEvent(new CustomEvent('closeDropdowns'))
|
||||||
|
}
|
||||||
isDropdownOpen = !isDropdownOpen
|
isDropdownOpen = !isDropdownOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +68,10 @@
|
||||||
dispatch('delete', { project })
|
dispatch('delete', { project })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClickOutside() {
|
||||||
|
isDropdownOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
function handleCloseDropdowns() {
|
function handleCloseDropdowns() {
|
||||||
isDropdownOpen = false
|
isDropdownOpen = false
|
||||||
|
|
@ -99,7 +108,11 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown-container">
|
<div
|
||||||
|
class="dropdown-container"
|
||||||
|
use:clickOutside={{ enabled: isDropdownOpen }}
|
||||||
|
on:clickoutside={handleClickOutside}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'
|
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'
|
||||||
|
import { clickOutside } from '$lib/actions/clickOutside'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
|
@ -97,30 +98,10 @@
|
||||||
applyColor(color)
|
applyColor(color)
|
||||||
onClose()
|
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>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="bubble-color-picker">
|
<div class="bubble-color-picker" use:clickOutside on:clickoutside={onClose}>
|
||||||
<div class="color-picker-header">
|
<div class="color-picker-header">
|
||||||
<span>{mode === 'text' ? 'Text Color' : 'Highlight Color'}</span>
|
<span>{mode === 'text' ? 'Text Color' : 'Highlight Color'}</span>
|
||||||
<button class="remove-color-btn" onclick={removeColor}> Remove </button>
|
<button class="remove-color-btn" onclick={removeColor}> Remove </button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
|
import { clickOutside } from '$lib/actions/clickOutside'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
|
@ -67,29 +68,10 @@
|
||||||
action()
|
action()
|
||||||
onClose()
|
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>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="bubble-text-style-menu">
|
<div class="bubble-text-style-menu" use:clickOutside on:clickoutside={onClose}>
|
||||||
{#each textStyles as style}
|
{#each textStyles as style}
|
||||||
<button
|
<button
|
||||||
class="text-style-option"
|
class="text-style-option"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import { goto, invalidate } from '$app/navigation'
|
import { goto, invalidate } from '$app/navigation'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
|
|
@ -68,18 +67,6 @@ const statusFilterOptions = [
|
||||||
{ value: 'status-draft', label: 'Draft first' }
|
{ 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() {
|
function handleNewEssay() {
|
||||||
goto('/admin/posts/new?type=essay')
|
goto('/admin/posts/new?type=essay')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
|
|
@ -70,18 +69,6 @@
|
||||||
{ value: 'status-draft', label: 'Draft first' }
|
{ 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 }>) {
|
function handleEdit(event: CustomEvent<{ project: AdminProject }>) {
|
||||||
goto(`/admin/projects/${event.detail.project.id}/edit`)
|
goto(`/admin/projects/${event.detail.project.id}/edit`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue