@nuasite/cms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { CSS, LAYOUT, TIMING } from '../constants'
|
|
3
|
+
import { getCmsElementAtPosition, getComponentAtPosition, isNearElementEdge } from '../dom'
|
|
4
|
+
import { getComponentInstance } from '../manifest'
|
|
5
|
+
import * as signals from '../signals'
|
|
6
|
+
import { isEventOnCmsUI, usePositionTracking } from './utils'
|
|
7
|
+
|
|
8
|
+
export interface OutlineState {
|
|
9
|
+
visible: boolean
|
|
10
|
+
rect: DOMRect | null
|
|
11
|
+
isComponent: boolean
|
|
12
|
+
componentName: string | undefined
|
|
13
|
+
tagName: string | undefined
|
|
14
|
+
element: HTMLElement | null
|
|
15
|
+
/** CMS ID of the detected element */
|
|
16
|
+
cmsId: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const INITIAL_STATE: OutlineState = {
|
|
20
|
+
visible: false,
|
|
21
|
+
rect: null,
|
|
22
|
+
isComponent: false,
|
|
23
|
+
componentName: undefined,
|
|
24
|
+
tagName: undefined,
|
|
25
|
+
element: null,
|
|
26
|
+
cmsId: null,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Hook for detecting and tracking hovered CMS elements.
|
|
31
|
+
* Uses signals directly for state management.
|
|
32
|
+
*/
|
|
33
|
+
export function useElementDetection(): OutlineState {
|
|
34
|
+
const [outlineState, setOutlineState] = useState<OutlineState>(INITIAL_STATE)
|
|
35
|
+
|
|
36
|
+
// Throttle ref for element detection
|
|
37
|
+
const lastDetectionTime = useRef<number>(0)
|
|
38
|
+
// Timeout for delayed hide (allows reaching color swatches)
|
|
39
|
+
const hideTimeoutRef = useRef<number | null>(null)
|
|
40
|
+
|
|
41
|
+
// Handle position updates on scroll/resize
|
|
42
|
+
const handlePositionChange = useCallback((rect: DOMRect | null) => {
|
|
43
|
+
if (rect) {
|
|
44
|
+
setOutlineState(prev => ({ ...prev, rect }))
|
|
45
|
+
} else {
|
|
46
|
+
setOutlineState(INITIAL_STATE)
|
|
47
|
+
}
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
// Track element position on scroll/resize
|
|
51
|
+
usePositionTracking(
|
|
52
|
+
outlineState.element,
|
|
53
|
+
handlePositionChange,
|
|
54
|
+
outlineState.visible,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Setup hover highlight for both elements and components
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
60
|
+
const isEditing = signals.isEditing.value
|
|
61
|
+
const chatOpen = signals.isChatOpen.value
|
|
62
|
+
|
|
63
|
+
if (!isEditing && !chatOpen) {
|
|
64
|
+
if (hideTimeoutRef.current) {
|
|
65
|
+
clearTimeout(hideTimeoutRef.current)
|
|
66
|
+
hideTimeoutRef.current = null
|
|
67
|
+
}
|
|
68
|
+
setOutlineState(prev => prev.visible ? INITIAL_STATE : prev)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if hovering over CMS UI, swatches, or attribute button - keep current state
|
|
73
|
+
if (isEventOnCmsUI(ev) || signals.isHoveringOutlineUI.value) {
|
|
74
|
+
// Cancel any pending hide since we're on UI elements
|
|
75
|
+
if (hideTimeoutRef.current) {
|
|
76
|
+
clearTimeout(hideTimeoutRef.current)
|
|
77
|
+
hideTimeoutRef.current = null
|
|
78
|
+
}
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Throttle detection for performance
|
|
83
|
+
const now = Date.now()
|
|
84
|
+
if (now - lastDetectionTime.current < TIMING.ELEMENT_DETECTION_THROTTLE_MS) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
lastDetectionTime.current = now
|
|
88
|
+
|
|
89
|
+
const manifest = signals.manifest.value
|
|
90
|
+
const entries = manifest.entries
|
|
91
|
+
|
|
92
|
+
// When chat is open, only detect components (not text/image elements)
|
|
93
|
+
if (chatOpen) {
|
|
94
|
+
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
95
|
+
if (componentEl) {
|
|
96
|
+
if (hideTimeoutRef.current) {
|
|
97
|
+
clearTimeout(hideTimeoutRef.current)
|
|
98
|
+
hideTimeoutRef.current = null
|
|
99
|
+
}
|
|
100
|
+
const rect = componentEl.getBoundingClientRect()
|
|
101
|
+
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
102
|
+
const instance = componentId ? getComponentInstance(manifest, componentId) : null
|
|
103
|
+
|
|
104
|
+
setOutlineState({
|
|
105
|
+
visible: true,
|
|
106
|
+
rect,
|
|
107
|
+
isComponent: true,
|
|
108
|
+
componentName: instance?.componentName,
|
|
109
|
+
tagName: componentEl.tagName.toLowerCase(),
|
|
110
|
+
element: componentEl,
|
|
111
|
+
cmsId: null,
|
|
112
|
+
})
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setOutlineState(INITIAL_STATE)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use the improved elementsFromPoint-based detection
|
|
121
|
+
const cmsEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
|
|
122
|
+
|
|
123
|
+
if (cmsEl && !cmsEl.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
|
|
124
|
+
// Found a text-editable element - cancel any pending hide
|
|
125
|
+
if (hideTimeoutRef.current) {
|
|
126
|
+
clearTimeout(hideTimeoutRef.current)
|
|
127
|
+
hideTimeoutRef.current = null
|
|
128
|
+
}
|
|
129
|
+
const rect = cmsEl.getBoundingClientRect()
|
|
130
|
+
const cmsId = cmsEl.getAttribute(CSS.ID_ATTRIBUTE)
|
|
131
|
+
setOutlineState({
|
|
132
|
+
visible: true,
|
|
133
|
+
rect,
|
|
134
|
+
isComponent: false,
|
|
135
|
+
componentName: undefined,
|
|
136
|
+
tagName: undefined,
|
|
137
|
+
element: cmsEl,
|
|
138
|
+
cmsId,
|
|
139
|
+
})
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for component at position
|
|
144
|
+
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
145
|
+
if (componentEl) {
|
|
146
|
+
const rect = componentEl.getBoundingClientRect()
|
|
147
|
+
const nearEdge = isNearElementEdge(
|
|
148
|
+
ev.clientX,
|
|
149
|
+
ev.clientY,
|
|
150
|
+
rect,
|
|
151
|
+
LAYOUT.COMPONENT_EDGE_THRESHOLD,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if (ev.altKey || nearEdge) {
|
|
155
|
+
// Cancel any pending hide
|
|
156
|
+
if (hideTimeoutRef.current) {
|
|
157
|
+
clearTimeout(hideTimeoutRef.current)
|
|
158
|
+
hideTimeoutRef.current = null
|
|
159
|
+
}
|
|
160
|
+
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
161
|
+
const instance = componentId ? getComponentInstance(manifest, componentId) : null
|
|
162
|
+
|
|
163
|
+
setOutlineState({
|
|
164
|
+
visible: true,
|
|
165
|
+
rect,
|
|
166
|
+
isComponent: true,
|
|
167
|
+
componentName: instance?.componentName,
|
|
168
|
+
tagName: componentEl.tagName.toLowerCase(),
|
|
169
|
+
element: componentEl,
|
|
170
|
+
cmsId: null,
|
|
171
|
+
})
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if current outline has color swatches or attribute button - if so, delay hide
|
|
177
|
+
setOutlineState(prev => {
|
|
178
|
+
if (prev.visible && prev.cmsId) {
|
|
179
|
+
const entry = manifest.entries[prev.cmsId]
|
|
180
|
+
const hasColorClasses = entry?.colorClasses?.bg?.value || entry?.colorClasses?.text?.value
|
|
181
|
+
const hasEditableAttributes = entry?.attributes && Object.keys(entry.attributes).length > 0
|
|
182
|
+
|
|
183
|
+
if ((hasColorClasses || hasEditableAttributes) && !hideTimeoutRef.current) {
|
|
184
|
+
// Schedule delayed hide to allow reaching swatches/attribute button
|
|
185
|
+
hideTimeoutRef.current = window.setTimeout(() => {
|
|
186
|
+
hideTimeoutRef.current = null
|
|
187
|
+
// Only hide if still not hovering over any outline UI
|
|
188
|
+
if (!signals.isHoveringOutlineUI.value) {
|
|
189
|
+
setOutlineState(INITIAL_STATE)
|
|
190
|
+
}
|
|
191
|
+
}, 400) // Delay to allow reaching buttons below element
|
|
192
|
+
return prev // Keep visible for now
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return INITIAL_STATE
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
document.addEventListener('mousemove', handleMouseMove, true)
|
|
200
|
+
return () => {
|
|
201
|
+
document.removeEventListener('mousemove', handleMouseMove, true)
|
|
202
|
+
if (hideTimeoutRef.current) {
|
|
203
|
+
clearTimeout(hideTimeoutRef.current)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, [])
|
|
207
|
+
|
|
208
|
+
return outlineState
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ComponentClickHandlerOptions {
|
|
212
|
+
onComponentSelect: (componentId: string, rect: DOMRect) => void
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Hook for handling component click selection.
|
|
217
|
+
* Uses signals directly for state management.
|
|
218
|
+
*/
|
|
219
|
+
export function useComponentClickHandler({
|
|
220
|
+
onComponentSelect,
|
|
221
|
+
}: ComponentClickHandlerOptions): void {
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
const handleClick = (ev: MouseEvent) => {
|
|
224
|
+
const isEditing = signals.isEditing.value
|
|
225
|
+
const chatOpen = signals.isChatOpen.value
|
|
226
|
+
if (!isEditing && !chatOpen) return
|
|
227
|
+
|
|
228
|
+
// Ignore clicks on CMS UI elements
|
|
229
|
+
if (isEventOnCmsUI(ev)) return
|
|
230
|
+
|
|
231
|
+
const manifest = signals.manifest.value
|
|
232
|
+
const entries = manifest.entries
|
|
233
|
+
|
|
234
|
+
if (chatOpen) {
|
|
235
|
+
// When chat is open, only select components
|
|
236
|
+
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
237
|
+
if (componentEl) {
|
|
238
|
+
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
239
|
+
if (componentId) {
|
|
240
|
+
ev.preventDefault()
|
|
241
|
+
ev.stopPropagation()
|
|
242
|
+
signals.setChatContextElement(componentId)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Normal editing mode behavior
|
|
249
|
+
// Check for text element first
|
|
250
|
+
const textEl = getCmsElementAtPosition(ev.clientX, ev.clientY, entries)
|
|
251
|
+
if (textEl && !textEl.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) {
|
|
252
|
+
// Let the text element handle this click
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check for component click
|
|
257
|
+
const componentEl = getComponentAtPosition(ev.clientX, ev.clientY)
|
|
258
|
+
if (componentEl) {
|
|
259
|
+
const rect = componentEl.getBoundingClientRect()
|
|
260
|
+
const nearEdge = isNearElementEdge(
|
|
261
|
+
ev.clientX,
|
|
262
|
+
ev.clientY,
|
|
263
|
+
rect,
|
|
264
|
+
LAYOUT.COMPONENT_EDGE_THRESHOLD,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if (ev.altKey || nearEdge) {
|
|
268
|
+
const componentId = componentEl.getAttribute(CSS.COMPONENT_ID_ATTRIBUTE)
|
|
269
|
+
if (componentId) {
|
|
270
|
+
ev.preventDefault()
|
|
271
|
+
ev.stopPropagation()
|
|
272
|
+
onComponentSelect(componentId, rect)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
document.addEventListener('click', handleClick, true)
|
|
279
|
+
return () => document.removeEventListener('click', handleClick, true)
|
|
280
|
+
}, [onComponentSelect])
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Re-export utilities for backwards compatibility
|
|
284
|
+
export { isEventOnCmsUI }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { CSS, TIMING } from '../constants'
|
|
3
|
+
import * as signals from '../signals'
|
|
4
|
+
import { isEventOnCmsUI, usePositionTracking } from './utils'
|
|
5
|
+
|
|
6
|
+
export interface ImageHoverState {
|
|
7
|
+
visible: boolean
|
|
8
|
+
rect: DOMRect | null
|
|
9
|
+
element: HTMLImageElement | null
|
|
10
|
+
cmsId: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const INITIAL_STATE: ImageHoverState = {
|
|
14
|
+
visible: false,
|
|
15
|
+
rect: null,
|
|
16
|
+
element: null,
|
|
17
|
+
cmsId: null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook for detecting and tracking hovered CMS image elements.
|
|
22
|
+
* Shows a visual overlay when hovering over images marked with data-cms-img.
|
|
23
|
+
*/
|
|
24
|
+
export function useImageHoverDetection(): ImageHoverState {
|
|
25
|
+
const [imageHoverState, setImageHoverState] = useState<ImageHoverState>(INITIAL_STATE)
|
|
26
|
+
|
|
27
|
+
// Throttle ref for element detection
|
|
28
|
+
const lastDetectionTime = useRef<number>(0)
|
|
29
|
+
|
|
30
|
+
// Handle position updates on scroll/resize
|
|
31
|
+
const handlePositionChange = useCallback((rect: DOMRect | null) => {
|
|
32
|
+
if (rect) {
|
|
33
|
+
setImageHoverState(prev => ({ ...prev, rect }))
|
|
34
|
+
} else {
|
|
35
|
+
setImageHoverState(INITIAL_STATE)
|
|
36
|
+
}
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
// Track element position on scroll/resize
|
|
40
|
+
usePositionTracking(
|
|
41
|
+
imageHoverState.element,
|
|
42
|
+
handlePositionChange,
|
|
43
|
+
imageHoverState.visible,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Setup hover detection for image elements
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
49
|
+
const isEditing = signals.isEditing.value
|
|
50
|
+
|
|
51
|
+
if (!isEditing) {
|
|
52
|
+
setImageHoverState(prev => prev.visible ? INITIAL_STATE : prev)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if hovering over CMS UI - keep current state
|
|
57
|
+
if (isEventOnCmsUI(ev)) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Throttle detection for performance
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
if (now - lastDetectionTime.current < TIMING.ELEMENT_DETECTION_THROTTLE_MS) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
lastDetectionTime.current = now
|
|
67
|
+
|
|
68
|
+
// Check if hovering over an image element with data-cms-img attribute
|
|
69
|
+
const elements = document.elementsFromPoint(ev.clientX, ev.clientY)
|
|
70
|
+
|
|
71
|
+
for (const el of elements) {
|
|
72
|
+
// If there's a contentEditable element above the image, don't show overlay
|
|
73
|
+
// This allows text editing on elements positioned over images
|
|
74
|
+
if (el instanceof HTMLElement && el.contentEditable === 'true') {
|
|
75
|
+
setImageHoverState(INITIAL_STATE)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (el instanceof HTMLImageElement && el.hasAttribute('data-cms-img')) {
|
|
80
|
+
const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
|
|
81
|
+
const rect = el.getBoundingClientRect()
|
|
82
|
+
|
|
83
|
+
setImageHoverState({
|
|
84
|
+
visible: true,
|
|
85
|
+
rect,
|
|
86
|
+
element: el,
|
|
87
|
+
cmsId,
|
|
88
|
+
})
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// No image found, hide overlay
|
|
94
|
+
setImageHoverState(INITIAL_STATE)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
document.addEventListener('mousemove', handleMouseMove, true)
|
|
98
|
+
return () => document.removeEventListener('mousemove', handleMouseMove, true)
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
return imageHoverState
|
|
102
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { CSS, TIMING } from '../constants'
|
|
3
|
+
import * as signals from '../signals'
|
|
4
|
+
import { getTextSelection, type TextSelection } from '../text-styling'
|
|
5
|
+
import { isElementInCmsUI, usePositionTracking } from './utils'
|
|
6
|
+
|
|
7
|
+
export interface TextSelectionState {
|
|
8
|
+
hasSelection: boolean
|
|
9
|
+
selection: TextSelection | null
|
|
10
|
+
rect: DOMRect | null
|
|
11
|
+
element: HTMLElement | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const INITIAL_STATE: TextSelectionState = {
|
|
15
|
+
hasSelection: false,
|
|
16
|
+
selection: null,
|
|
17
|
+
rect: null,
|
|
18
|
+
element: null,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook for managing text selection state within CMS elements.
|
|
23
|
+
* Tracks when the user has selected text and provides the selection details
|
|
24
|
+
* for the text styling toolbar.
|
|
25
|
+
*/
|
|
26
|
+
export function useTextSelection(): TextSelectionState {
|
|
27
|
+
const [state, setState] = useState<TextSelectionState>(INITIAL_STATE)
|
|
28
|
+
|
|
29
|
+
// Track the last active element to detect if user clicked on CMS UI
|
|
30
|
+
const lastActiveElementRef = useRef<Element | null>(null)
|
|
31
|
+
|
|
32
|
+
// Update rect on scroll - using a simpler approach since selection rect
|
|
33
|
+
// needs to be recalculated from the Selection API, not tracked element
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!state.hasSelection) return
|
|
36
|
+
|
|
37
|
+
const updateRect = () => {
|
|
38
|
+
const selection = window.getSelection()
|
|
39
|
+
if (selection && selection.rangeCount > 0) {
|
|
40
|
+
const range = selection.getRangeAt(0)
|
|
41
|
+
const rect = range.getBoundingClientRect()
|
|
42
|
+
setState(prev => ({ ...prev, rect }))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
window.addEventListener('scroll', updateRect, true)
|
|
47
|
+
window.addEventListener('resize', updateRect)
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener('scroll', updateRect, true)
|
|
51
|
+
window.removeEventListener('resize', updateRect)
|
|
52
|
+
}
|
|
53
|
+
}, [state.hasSelection])
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const processSelection = () => {
|
|
57
|
+
const isEditing = signals.isEditing.value
|
|
58
|
+
if (!isEditing) {
|
|
59
|
+
setState(INITIAL_STATE)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const selection = window.getSelection()
|
|
64
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
65
|
+
// Check if focus is on CMS UI element - if so, preserve the selection state
|
|
66
|
+
// This allows clicking toolbar buttons without losing selection
|
|
67
|
+
const activeElement = document.activeElement
|
|
68
|
+
if (activeElement && isElementInCmsUI(activeElement as HTMLElement)) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
setState(INITIAL_STATE)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const range = selection.getRangeAt(0)
|
|
76
|
+
const container = range.commonAncestorContainer
|
|
77
|
+
|
|
78
|
+
// Find the CMS element that contains the selection
|
|
79
|
+
let element: HTMLElement | null = container.nodeType === Node.TEXT_NODE
|
|
80
|
+
? container.parentElement
|
|
81
|
+
: container as HTMLElement
|
|
82
|
+
|
|
83
|
+
let cmsElement: HTMLElement | null = null
|
|
84
|
+
while (element && element !== document.body) {
|
|
85
|
+
if (element.hasAttribute(CSS.ID_ATTRIBUTE) && element.contentEditable === 'true') {
|
|
86
|
+
cmsElement = element
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
element = element.parentElement
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!cmsElement) {
|
|
93
|
+
setState(INITIAL_STATE)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const textSelection = getTextSelection(cmsElement)
|
|
98
|
+
if (!textSelection) {
|
|
99
|
+
setState(INITIAL_STATE)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get the bounding rect of the selection
|
|
104
|
+
const rect = range.getBoundingClientRect()
|
|
105
|
+
|
|
106
|
+
setState({
|
|
107
|
+
hasSelection: true,
|
|
108
|
+
selection: textSelection,
|
|
109
|
+
rect,
|
|
110
|
+
element: cmsElement,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Debounce selection change handling
|
|
115
|
+
let timeoutId: number | null = null
|
|
116
|
+
const debouncedHandler = () => {
|
|
117
|
+
if (timeoutId) {
|
|
118
|
+
clearTimeout(timeoutId)
|
|
119
|
+
}
|
|
120
|
+
timeoutId = window.setTimeout(processSelection, TIMING.BLUR_DELAY_MS)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle mousedown to track where user clicked
|
|
124
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
125
|
+
lastActiveElementRef.current = e.target as Element
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle mouseup for immediate feedback after selection
|
|
129
|
+
const handleMouseUp = (e: MouseEvent) => {
|
|
130
|
+
const target = e.target as HTMLElement
|
|
131
|
+
|
|
132
|
+
// Don't process if clicking on CMS UI - let the selection persist
|
|
133
|
+
if (isElementInCmsUI(target)) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Process selection after a brief delay for the browser to update selection
|
|
138
|
+
window.setTimeout(processSelection, TIMING.BLUR_DELAY_MS)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
document.addEventListener('selectionchange', debouncedHandler)
|
|
142
|
+
document.addEventListener('mousedown', handleMouseDown, true)
|
|
143
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
144
|
+
|
|
145
|
+
return () => {
|
|
146
|
+
document.removeEventListener('selectionchange', debouncedHandler)
|
|
147
|
+
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
148
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
149
|
+
if (timeoutId) {
|
|
150
|
+
clearTimeout(timeoutId)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}, [])
|
|
154
|
+
|
|
155
|
+
return state
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Clear the current text selection
|
|
160
|
+
*/
|
|
161
|
+
export function clearTextSelection(): void {
|
|
162
|
+
const selection = window.getSelection()
|
|
163
|
+
if (selection) {
|
|
164
|
+
selection.removeAllRanges()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Save and restore selection - useful when applying styles
|
|
170
|
+
*/
|
|
171
|
+
export function saveSelection(): Range | null {
|
|
172
|
+
const selection = window.getSelection()
|
|
173
|
+
if (selection && selection.rangeCount > 0) {
|
|
174
|
+
return selection.getRangeAt(0).cloneRange()
|
|
175
|
+
}
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function restoreSelection(range: Range | null): void {
|
|
180
|
+
if (!range) return
|
|
181
|
+
|
|
182
|
+
const selection = window.getSelection()
|
|
183
|
+
if (selection) {
|
|
184
|
+
selection.removeAllRanges()
|
|
185
|
+
selection.addRange(range)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'preact/hooks'
|
|
2
|
+
import { CSS } from '../constants'
|
|
3
|
+
import * as signals from '../signals'
|
|
4
|
+
|
|
5
|
+
export interface TooltipState {
|
|
6
|
+
elementId: string | null
|
|
7
|
+
rect: DOMRect | null
|
|
8
|
+
element: HTMLElement | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseTooltipStateOptions {
|
|
12
|
+
/** @deprecated No longer needed - signals are used directly */
|
|
13
|
+
isEditing?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook for managing tooltip visibility and positioning.
|
|
18
|
+
* Uses signals directly for state management.
|
|
19
|
+
*/
|
|
20
|
+
export function useTooltipState(_options?: UseTooltipStateOptions) {
|
|
21
|
+
const [tooltipState, setTooltipState] = useState<TooltipState>({
|
|
22
|
+
elementId: null,
|
|
23
|
+
rect: null,
|
|
24
|
+
element: null,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Show tooltip for the current editing element
|
|
29
|
+
*/
|
|
30
|
+
const showTooltipForElement = useCallback(() => {
|
|
31
|
+
const currentEditingId = signals.currentEditingId.value
|
|
32
|
+
const isProcessing = signals.isAIProcessing.value
|
|
33
|
+
|
|
34
|
+
if (!currentEditingId || isProcessing) {
|
|
35
|
+
setTooltipState({ elementId: null, rect: null, element: null })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const change = signals.getPendingChange(currentEditingId)
|
|
40
|
+
if (!change) {
|
|
41
|
+
setTooltipState({ elementId: null, rect: null, element: null })
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setTooltipState({
|
|
46
|
+
elementId: currentEditingId,
|
|
47
|
+
rect: change.element.getBoundingClientRect(),
|
|
48
|
+
element: change.element,
|
|
49
|
+
})
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hide the tooltip
|
|
54
|
+
*/
|
|
55
|
+
const hideTooltip = useCallback(() => {
|
|
56
|
+
setTooltipState({ elementId: null, rect: null, element: null })
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
// Update tooltip position on scroll
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!tooltipState.elementId || !tooltipState.element) return
|
|
62
|
+
|
|
63
|
+
const updateTooltipPosition = () => {
|
|
64
|
+
if (tooltipState.element && document.contains(tooltipState.element)) {
|
|
65
|
+
setTooltipState(prev => ({
|
|
66
|
+
...prev,
|
|
67
|
+
rect: tooltipState.element!.getBoundingClientRect(),
|
|
68
|
+
}))
|
|
69
|
+
} else {
|
|
70
|
+
// Element no longer in DOM
|
|
71
|
+
setTooltipState({ elementId: null, rect: null, element: null })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Hide tooltip when clicking outside the element and CMS UI
|
|
76
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
77
|
+
const path = e.composedPath()
|
|
78
|
+
const target = path[0] as HTMLElement
|
|
79
|
+
|
|
80
|
+
// Check if click is on the tooltip element itself
|
|
81
|
+
if (tooltipState.element?.contains(target) || tooltipState.element === target) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if any element in the path is inside CMS UI
|
|
86
|
+
const cmsOverlay = document.querySelector(CSS.HIGHLIGHT_ELEMENT)
|
|
87
|
+
for (const el of path) {
|
|
88
|
+
if (el === cmsOverlay) {
|
|
89
|
+
return // Click was inside Shadow DOM
|
|
90
|
+
}
|
|
91
|
+
if (el instanceof HTMLElement) {
|
|
92
|
+
if (el.tagName?.startsWith('CMS-')) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (el.hasAttribute?.(CSS.UI_ATTRIBUTE)) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if click is on another CMS-editable element
|
|
102
|
+
if (target.hasAttribute?.(CSS.ID_ATTRIBUTE)) {
|
|
103
|
+
return // Will be handled by element focus
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Click was outside, hide tooltip
|
|
107
|
+
setTooltipState({ elementId: null, rect: null, element: null })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
window.addEventListener('scroll', updateTooltipPosition, true)
|
|
111
|
+
window.addEventListener('resize', updateTooltipPosition)
|
|
112
|
+
document.addEventListener('mousedown', handleClickOutside, true)
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
window.removeEventListener('scroll', updateTooltipPosition, true)
|
|
116
|
+
window.removeEventListener('resize', updateTooltipPosition)
|
|
117
|
+
document.removeEventListener('mousedown', handleClickOutside, true)
|
|
118
|
+
}
|
|
119
|
+
}, [tooltipState.elementId, tooltipState.element])
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
tooltipState,
|
|
123
|
+
showTooltipForElement,
|
|
124
|
+
hideTooltip,
|
|
125
|
+
}
|
|
126
|
+
}
|