@ai-stack/payloadcms 3.2.21-beta → 3.2.22-beta
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/dist/ui/Compose/hooks/menu/TranslateMenu.js +1 -2
- package/dist/ui/Compose/hooks/menu/TranslateMenu.js.map +1 -1
- package/dist/ui/Compose/hooks/menu/TranslateMenu.jsx +1 -1
- package/dist/ui/Compose/hooks/useActiveFieldTracking.js +98 -21
- package/dist/ui/Compose/hooks/useActiveFieldTracking.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../src/ui/Compose/hooks/menu/TranslateMenu.tsx"],"sourcesContent":["import locales from 'locale-codes'\nimport React, { memo, useState } from 'react'\n\nimport { useInstructions } from '../../../../providers/InstructionsProvider/useInstructions.js'\nimport { Item } from './Item.js'\nimport { Translate } from './items.js'\nimport styles from './menu.module.scss'\n\nexport const TranslateMenu = ({ onClick }: { onClick: (data: { locale: string }) => void }) => {\n const [show, setShow] = useState(false)\n\n const { enabledLanguages = [] } = useInstructions()\n\n let filteredLocales = locales.all.filter((a) => {\n return a.tag && a.location\n })\n\n if (enabledLanguages?.length) {\n filteredLocales = filteredLocales.filter((a) => enabledLanguages?.includes(a.tag))\n }\n\n const [languages, setLanguages] = useState(filteredLocales)\n const [inputFocus, setInputFocus] = useState(false)\n\n return (\n <div\n className={styles.menu}\n onMouseLeave={() => {\n if (!inputFocus) {\n setShow(false)\n }\n }}\n >\n <Translate\n isActive={show}\n isMenu\n onClick={() => {\n setShow(!show)\n }}\n
|
|
1
|
+
{"version":3,"sources":["../../../../../src/ui/Compose/hooks/menu/TranslateMenu.tsx"],"sourcesContent":["import locales from 'locale-codes'\nimport React, { memo, useState } from 'react'\n\nimport { useInstructions } from '../../../../providers/InstructionsProvider/useInstructions.js'\nimport { Item } from './Item.js'\nimport { Translate } from './items.js'\nimport styles from './menu.module.scss'\n\nexport const TranslateMenu = ({ onClick }: { onClick: (data: { locale: string }) => void }) => {\n const [show, setShow] = useState(false)\n\n const { enabledLanguages = [] } = useInstructions()\n\n let filteredLocales = locales.all.filter((a) => {\n return a.tag && a.location\n })\n\n if (enabledLanguages?.length) {\n filteredLocales = filteredLocales.filter((a) => enabledLanguages?.includes(a.tag))\n }\n\n const [languages, setLanguages] = useState(filteredLocales)\n const [inputFocus, setInputFocus] = useState(false)\n\n return (\n <div\n className={styles.menu}\n onMouseLeave={() => {\n if (!inputFocus) {\n setShow(false)\n }\n }}\n >\n <Translate\n isActive={show}\n isMenu\n onClick={() => {\n setShow(!show)\n }}\n />\n <div className={styles.hoverMenu} data-show={show}>\n <div\n className={`${styles.menu} ${styles.subMenu}`}\n style={{\n background: 'var(--popup-bg)',\n // minHeight: '300px',\n }}\n >\n <Item\n onClick={() => {}}\n style={{\n background: 'transparent',\n padding: '0 0 5px 0',\n position: 'sticky',\n top: 0,\n }}\n >\n <input\n className={styles.menuInput}\n onBlur={() => setInputFocus(false)}\n onChange={(event) => {\n const value = event.target.value\n setLanguages(\n filteredLocales.filter((l) => {\n const lowerCaseValue = value.toLowerCase()\n return (\n l.name.toLowerCase().startsWith(lowerCaseValue) ||\n (l.location && l.location.toLowerCase().startsWith(lowerCaseValue)) ||\n l.tag.toLowerCase().startsWith(lowerCaseValue)\n )\n }),\n )\n }}\n onFocus={() => setInputFocus(true)}\n placeholder=\"Search...\"\n />\n </Item>\n {languages.map((locale) => {\n return (\n <Item\n key={locale.tag}\n onClick={() => {\n onClick({ locale: locale.tag })\n }}\n >\n <span className={styles.ellipsis}>{`${locale.location} (${locale.tag})`}</span>\n </Item>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport const MemoizedTranslateMenu = memo(TranslateMenu)\n"],"names":["locales","React","memo","useState","useInstructions","Item","Translate","styles","TranslateMenu","onClick","show","setShow","enabledLanguages","filteredLocales","all","filter","a","tag","location","length","includes","languages","setLanguages","inputFocus","setInputFocus","div","className","menu","onMouseLeave","isActive","isMenu","hoverMenu","data-show","subMenu","style","background","padding","position","top","input","menuInput","onBlur","onChange","event","value","target","l","lowerCaseValue","toLowerCase","name","startsWith","onFocus","placeholder","map","locale","span","ellipsis","MemoizedTranslateMenu"],"mappings":";AAAA,OAAOA,aAAa,eAAc;AAClC,OAAOC,SAASC,IAAI,EAAEC,QAAQ,QAAQ,QAAO;AAE7C,SAASC,eAAe,QAAQ,gEAA+D;AAC/F,SAASC,IAAI,QAAQ,YAAW;AAChC,SAASC,SAAS,QAAQ,aAAY;AACtC,OAAOC,YAAY,qBAAoB;AAEvC,OAAO,MAAMC,gBAAgB,CAAC,EAAEC,OAAO,EAAmD;IACxF,MAAM,CAACC,MAAMC,QAAQ,GAAGR,SAAS;IAEjC,MAAM,EAAES,mBAAmB,EAAE,EAAE,GAAGR;IAElC,IAAIS,kBAAkBb,QAAQc,GAAG,CAACC,MAAM,CAAC,CAACC;QACxC,OAAOA,EAAEC,GAAG,IAAID,EAAEE,QAAQ;IAC5B;IAEA,IAAIN,kBAAkBO,QAAQ;QAC5BN,kBAAkBA,gBAAgBE,MAAM,CAAC,CAACC,IAAMJ,kBAAkBQ,SAASJ,EAAEC,GAAG;IAClF;IAEA,MAAM,CAACI,WAAWC,aAAa,GAAGnB,SAASU;IAC3C,MAAM,CAACU,YAAYC,cAAc,GAAGrB,SAAS;IAE7C,qBACE,MAACsB;QACCC,WAAWnB,OAAOoB,IAAI;QACtBC,cAAc;YACZ,IAAI,CAACL,YAAY;gBACfZ,QAAQ;YACV;QACF;;0BAEA,KAACL;gBACCuB,UAAUnB;gBACVoB,MAAM;gBACNrB,SAAS;oBACPE,QAAQ,CAACD;gBACX;;0BAEF,KAACe;gBAAIC,WAAWnB,OAAOwB,SAAS;gBAAEC,aAAWtB;0BAC3C,cAAA,MAACe;oBACCC,WAAW,GAAGnB,OAAOoB,IAAI,CAAC,CAAC,EAAEpB,OAAO0B,OAAO,EAAE;oBAC7CC,OAAO;wBACLC,YAAY;oBAEd;;sCAEA,KAAC9B;4BACCI,SAAS,KAAO;4BAChByB,OAAO;gCACLC,YAAY;gCACZC,SAAS;gCACTC,UAAU;gCACVC,KAAK;4BACP;sCAEA,cAAA,KAACC;gCACCb,WAAWnB,OAAOiC,SAAS;gCAC3BC,QAAQ,IAAMjB,cAAc;gCAC5BkB,UAAU,CAACC;oCACT,MAAMC,QAAQD,MAAME,MAAM,CAACD,KAAK;oCAChCtB,aACET,gBAAgBE,MAAM,CAAC,CAAC+B;wCACtB,MAAMC,iBAAiBH,MAAMI,WAAW;wCACxC,OACEF,EAAEG,IAAI,CAACD,WAAW,GAAGE,UAAU,CAACH,mBAC/BD,EAAE5B,QAAQ,IAAI4B,EAAE5B,QAAQ,CAAC8B,WAAW,GAAGE,UAAU,CAACH,mBACnDD,EAAE7B,GAAG,CAAC+B,WAAW,GAAGE,UAAU,CAACH;oCAEnC;gCAEJ;gCACAI,SAAS,IAAM3B,cAAc;gCAC7B4B,aAAY;;;wBAGf/B,UAAUgC,GAAG,CAAC,CAACC;4BACd,qBACE,KAACjD;gCAECI,SAAS;oCACPA,QAAQ;wCAAE6C,QAAQA,OAAOrC,GAAG;oCAAC;gCAC/B;0CAEA,cAAA,KAACsC;oCAAK7B,WAAWnB,OAAOiD,QAAQ;8CAAG,GAAGF,OAAOpC,QAAQ,CAAC,EAAE,EAAEoC,OAAOrC,GAAG,CAAC,CAAC,CAAC;;+BALlEqC,OAAOrC,GAAG;wBAQrB;;;;;;AAKV,EAAC;AAED,OAAO,MAAMwC,sCAAwBvD,KAAKM,eAAc"}
|
|
@@ -22,7 +22,7 @@ export const TranslateMenu = ({ onClick }) => {
|
|
|
22
22
|
}}>
|
|
23
23
|
<Translate isActive={show} isMenu onClick={() => {
|
|
24
24
|
setShow(!show);
|
|
25
|
-
}}
|
|
25
|
+
}}/>
|
|
26
26
|
<div className={styles.hoverMenu} data-show={show}>
|
|
27
27
|
<div className={`${styles.menu} ${styles.subMenu}`} style={{
|
|
28
28
|
background: 'var(--popup-bg)',
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Allowed field type classes that should show the active state
|
|
5
|
+
*/ const ALLOWED_FIELD_TYPES = [
|
|
6
|
+
'upload',
|
|
7
|
+
'text',
|
|
8
|
+
'textarea',
|
|
9
|
+
'rich-text-lexical'
|
|
10
|
+
];
|
|
3
11
|
let currentContainer = null;
|
|
12
|
+
let rafId = null // Track RAF to cancel if needed
|
|
13
|
+
;
|
|
4
14
|
/**
|
|
5
15
|
* Safely escape CSS selector values
|
|
6
16
|
*/ const cssEscape = (value)=>{
|
|
@@ -21,30 +31,62 @@ let currentContainer = null;
|
|
|
21
31
|
const control = document.querySelector(selector);
|
|
22
32
|
return control?.closest('.field-type') ?? null;
|
|
23
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Check if a container has one of the allowed field type classes
|
|
36
|
+
*/ const isAllowedFieldType = (container)=>{
|
|
37
|
+
return ALLOWED_FIELD_TYPES.some((type)=>container.classList.contains(type) || container.classList.contains(`field-type-${type}`));
|
|
38
|
+
};
|
|
24
39
|
/**
|
|
25
40
|
* Resolve the .field-type container for a given event target
|
|
41
|
+
* Only returns containers that match allowed field types
|
|
26
42
|
*/ const resolveContainerFromTarget = (target)=>{
|
|
27
43
|
if (!(target instanceof HTMLElement)) {
|
|
28
44
|
return null;
|
|
29
45
|
}
|
|
30
46
|
// Check for direct parent first
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
let container = target.closest('.field-type');
|
|
48
|
+
// If not found, fall back to React Select logic
|
|
49
|
+
if (!container) {
|
|
50
|
+
container = findContainerFromReactSelect(target);
|
|
34
51
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
52
|
+
// Only return if it's an allowed field type
|
|
53
|
+
if (container && isAllowedFieldType(container)) {
|
|
54
|
+
return container;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
37
57
|
};
|
|
38
58
|
/**
|
|
39
59
|
* Update the active container and toggle CSS class
|
|
60
|
+
* - Avoids acting on disconnected nodes
|
|
61
|
+
* - Avoids redundant class work
|
|
40
62
|
*/ const setActiveContainer = (next)=>{
|
|
63
|
+
// Normalize both references against disconnected nodes
|
|
64
|
+
if (currentContainer && !currentContainer.isConnected) {
|
|
65
|
+
currentContainer = null;
|
|
66
|
+
}
|
|
67
|
+
if (next && !next.isConnected) {
|
|
68
|
+
next = null;
|
|
69
|
+
}
|
|
41
70
|
if (currentContainer === next) {
|
|
42
71
|
return;
|
|
43
72
|
}
|
|
44
73
|
currentContainer?.classList.remove('ai-plugin-active');
|
|
45
|
-
next
|
|
74
|
+
if (next) {
|
|
75
|
+
next.classList.add('ai-plugin-active');
|
|
76
|
+
}
|
|
46
77
|
currentContainer = next;
|
|
47
78
|
};
|
|
79
|
+
const clearActiveContainer = ()=>{
|
|
80
|
+
if (currentContainer) {
|
|
81
|
+
currentContainer.classList.remove('ai-plugin-active');
|
|
82
|
+
currentContainer = null;
|
|
83
|
+
}
|
|
84
|
+
// Cancel any pending RAF
|
|
85
|
+
if (rafId !== null) {
|
|
86
|
+
cancelAnimationFrame(rafId);
|
|
87
|
+
rafId = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
48
90
|
const isInteractiveElement = (element)=>{
|
|
49
91
|
const tagName = element.tagName.toLowerCase();
|
|
50
92
|
const interactiveTags = [
|
|
@@ -79,15 +121,16 @@ const isInteractiveElement = (element)=>{
|
|
|
79
121
|
if (!(target instanceof HTMLElement)) {
|
|
80
122
|
return;
|
|
81
123
|
}
|
|
124
|
+
// Early exit if we're already inside the current container
|
|
125
|
+
if (currentContainer?.isConnected && currentContainer.contains(target)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
82
128
|
// Only activate if the focused element is actually interactive
|
|
83
129
|
if (!isInteractiveElement(target)) {
|
|
84
130
|
return;
|
|
85
131
|
}
|
|
86
132
|
const container = resolveContainerFromTarget(target);
|
|
87
|
-
|
|
88
|
-
if (container) {
|
|
89
|
-
setActiveContainer(container);
|
|
90
|
-
}
|
|
133
|
+
setActiveContainer(container);
|
|
91
134
|
};
|
|
92
135
|
/**
|
|
93
136
|
* Handle pointer/mouse events - only switch when clicking a different .field-type
|
|
@@ -96,11 +139,12 @@ const isInteractiveElement = (element)=>{
|
|
|
96
139
|
if (!(target instanceof HTMLElement)) {
|
|
97
140
|
return;
|
|
98
141
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
setActiveContainer(container);
|
|
142
|
+
// Early exit if clicking within current container
|
|
143
|
+
if (currentContainer?.isConnected && currentContainer.contains(target)) {
|
|
144
|
+
return;
|
|
103
145
|
}
|
|
146
|
+
const container = resolveContainerFromTarget(target);
|
|
147
|
+
setActiveContainer(container);
|
|
104
148
|
};
|
|
105
149
|
/**
|
|
106
150
|
* Handle keyboard navigation (Tab key)
|
|
@@ -108,11 +152,25 @@ const isInteractiveElement = (element)=>{
|
|
|
108
152
|
if (e.key !== 'Tab') {
|
|
109
153
|
return;
|
|
110
154
|
}
|
|
111
|
-
|
|
155
|
+
// Cancel any pending RAF to prevent queuing
|
|
156
|
+
if (rafId !== null) {
|
|
157
|
+
cancelAnimationFrame(rafId);
|
|
158
|
+
}
|
|
159
|
+
// Defer until after focus has shifted
|
|
160
|
+
rafId = requestAnimationFrame(()=>{
|
|
161
|
+
rafId = null;
|
|
112
162
|
const container = resolveContainerFromTarget(document.activeElement);
|
|
113
163
|
setActiveContainer(container);
|
|
114
164
|
});
|
|
115
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* Handle visibility changes to properly cleanup when page is hidden
|
|
168
|
+
*/ const onVisibilityChange = ()=>{
|
|
169
|
+
if (typeof document !== 'undefined' && document.hidden) {
|
|
170
|
+
// Clear active state and cancel pending operations
|
|
171
|
+
clearActiveContainer();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
116
174
|
/**
|
|
117
175
|
* Initialize document-level listeners to track the active field container.
|
|
118
176
|
* When a container is active, it receives the 'ai-plugin-active' class.
|
|
@@ -126,18 +184,37 @@ const isInteractiveElement = (element)=>{
|
|
|
126
184
|
pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 0) + 1;
|
|
127
185
|
// Initialize listeners only once
|
|
128
186
|
if (!pluginWindow.__aiComposeTracking) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
pluginWindow.__aiComposeTrackingController = controller;
|
|
189
|
+
// Use capture for early handling
|
|
190
|
+
document.addEventListener('focusin', onFocusIn, {
|
|
191
|
+
capture: true,
|
|
192
|
+
signal: controller.signal
|
|
193
|
+
});
|
|
194
|
+
document.addEventListener('pointerdown', onPointerDown, {
|
|
195
|
+
capture: true,
|
|
196
|
+
passive: true,
|
|
197
|
+
signal: controller.signal
|
|
198
|
+
});
|
|
199
|
+
document.addEventListener('keydown', onKeyDown, {
|
|
200
|
+
capture: true,
|
|
201
|
+
signal: controller.signal
|
|
202
|
+
});
|
|
203
|
+
document.addEventListener('visibilitychange', onVisibilityChange, {
|
|
204
|
+
signal: controller.signal
|
|
205
|
+
});
|
|
132
206
|
pluginWindow.__aiComposeTracking = true;
|
|
133
207
|
}
|
|
134
208
|
return ()=>{
|
|
135
209
|
// Decrement and cleanup when the last user unmounts
|
|
136
210
|
pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 1) - 1;
|
|
137
211
|
if ((pluginWindow.__aiComposeTrackingCount ?? 0) <= 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
212
|
+
// Atomically remove all listeners
|
|
213
|
+
pluginWindow.__aiComposeTrackingController?.abort();
|
|
214
|
+
pluginWindow.__aiComposeTrackingController = undefined;
|
|
215
|
+
// Clear active state and cancel pending operations
|
|
216
|
+
clearActiveContainer();
|
|
217
|
+
// Reset all state
|
|
141
218
|
pluginWindow.__aiComposeTracking = false;
|
|
142
219
|
pluginWindow.__aiComposeTrackingCount = 0;
|
|
143
220
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/Compose/hooks/useActiveFieldTracking.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\nlet currentContainer: HTMLElement | null = null\n\n/**\n * Safely escape CSS selector values\n */\nconst cssEscape = (value: string): string => {\n if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {\n return CSS.escape(value)\n }\n return value.replace(/([ #;?%&,.+*~':\"!^$[\\]()=>|/@])/g, '\\\\$1')\n}\n\n/**\n * Find container from React Select dropdown elements\n */\nconst findContainerFromReactSelect = (target: HTMLElement): HTMLElement | null => {\n const listbox = target.closest<HTMLElement>('[role=\"listbox\"]')\n if (!listbox?.id) {\n return null\n }\n\n const id = cssEscape(listbox.id)\n const selector = `[aria-controls=\"${id}\"], [aria-owns=\"${id}\"]`\n const control = document.querySelector<HTMLElement>(selector)\n\n return control?.closest<HTMLElement>('.field-type') ?? null\n}\n\n/**\n * Resolve the .field-type container for a given event target\n */\nconst resolveContainerFromTarget = (target: EventTarget | null): HTMLElement | null => {\n if (!(target instanceof HTMLElement)) {\n return null\n }\n\n // Check for direct parent first\n const direct = target.closest<HTMLElement>('.field-type')\n if (direct) {\n return direct\n }\n\n // Fall back to React Select logic\n return findContainerFromReactSelect(target)\n}\n\n/**\n * Update the active container and toggle CSS class\n */\nconst setActiveContainer = (next: HTMLElement | null): void => {\n if (currentContainer === next) {\n return\n }\n\n currentContainer?.classList.remove('ai-plugin-active')\n next?.classList.add('ai-plugin-active')\n currentContainer = next\n}\n\nconst isInteractiveElement = (element: HTMLElement): boolean => {\n const tagName = element.tagName.toLowerCase()\n const interactiveTags = ['input', 'textarea', 'select', 'button']\n\n if (interactiveTags.includes(tagName)) {\n return true\n }\n\n // Check for contenteditable\n if (element.isContentEditable) {\n return true\n }\n\n // Check for elements with role=\"textbox\" or role=\"combobox\" (React Select)\n const role = element.getAttribute('role')\n if (role && ['combobox', 'listbox', 'searchbox', 'textbox'].includes(role)) {\n return true\n }\n\n return false\n}\n\n/**\n * Handle focus events - only activate if focus is on an interactive element within .field-type\n */\nconst onFocusIn = (e: FocusEvent): void => {\n const target = e.target\n if (!(target instanceof HTMLElement)) {\n return\n }\n\n // Only activate if the focused element is actually interactive\n if (!isInteractiveElement(target)) {\n return\n }\n\n const container = resolveContainerFromTarget(target)\n // Only update if we found a new container\n if (container) {\n setActiveContainer(container)\n }\n}\n\n/**\n * Handle pointer/mouse events - only switch when clicking a different .field-type\n */\nconst onPointerDown = (e: PointerEvent): void => {\n const target = e.target\n if (!(target instanceof HTMLElement)) {\n return\n }\n\n const container = resolveContainerFromTarget(target)\n\n // Only update if we found a container (keeps last active if clicking elsewhere)\n if (container) {\n setActiveContainer(container)\n }\n}\n\n/**\n * Handle keyboard navigation (Tab key)\n */\nconst onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== 'Tab') {\n return\n }\n\n requestAnimationFrame(() => {\n const container = resolveContainerFromTarget(document.activeElement)\n setActiveContainer(container)\n })\n}\n\n/**\n * Initialize document-level listeners to track the active field container.\n * When a container is active, it receives the 'ai-plugin-active' class.\n */\nexport const useActiveFieldTracking = (): void => {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return\n }\n\n const pluginWindow = window as {\n __aiComposeTracking?: boolean\n __aiComposeTrackingCount?: number\n } & Window\n\n // Track number of mounted users of the hook\n pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 0) + 1\n\n // Initialize listeners only once\n if (!pluginWindow.__aiComposeTracking) {\n document.addEventListener('focusin', onFocusIn, true)\n document.addEventListener('pointerdown', onPointerDown, true)\n document.addEventListener('keydown', onKeyDown, true)\n pluginWindow.__aiComposeTracking = true\n }\n\n return () => {\n // Decrement and cleanup when the last user unmounts\n pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 1) - 1\n if ((pluginWindow.__aiComposeTrackingCount ?? 0) <= 0) {\n document.removeEventListener('focusin', onFocusIn, true)\n document.removeEventListener('pointerdown', onPointerDown, true)\n document.removeEventListener('keydown', onKeyDown, true)\n pluginWindow.__aiComposeTracking = false\n pluginWindow.__aiComposeTrackingCount = 0\n }\n }\n }, [])\n}\n"],"names":["useEffect","currentContainer","cssEscape","value","CSS","escape","replace","findContainerFromReactSelect","target","listbox","closest","id","selector","control","document","querySelector","resolveContainerFromTarget","HTMLElement","direct","setActiveContainer","next","classList","remove","add","isInteractiveElement","element","tagName","toLowerCase","interactiveTags","includes","isContentEditable","role","getAttribute","onFocusIn","e","container","onPointerDown","onKeyDown","key","requestAnimationFrame","activeElement","useActiveFieldTracking","window","pluginWindow","__aiComposeTrackingCount","__aiComposeTracking","addEventListener","removeEventListener"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/Compose/hooks/useActiveFieldTracking.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\n\n/**\n * Allowed field type classes that should show the active state\n */\nconst ALLOWED_FIELD_TYPES = ['upload', 'text', 'textarea', 'rich-text-lexical']\n\nlet currentContainer: HTMLElement | null = null\nlet rafId: null | number = null // Track RAF to cancel if needed\n\n/**\n * Safely escape CSS selector values\n */\nconst cssEscape = (value: string): string => {\n if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {\n return CSS.escape(value)\n }\n return value.replace(/([ #;?%&,.+*~':\"!^$[\\]()=>|/@])/g, '\\\\$1')\n}\n\n/**\n * Find container from React Select dropdown elements\n */\nconst findContainerFromReactSelect = (target: HTMLElement): HTMLElement | null => {\n const listbox = target.closest<HTMLElement>('[role=\"listbox\"]')\n if (!listbox?.id) {\n return null\n }\n\n const id = cssEscape(listbox.id)\n const selector = `[aria-controls=\"${id}\"], [aria-owns=\"${id}\"]`\n const control = document.querySelector<HTMLElement>(selector)\n\n return control?.closest<HTMLElement>('.field-type') ?? null\n}\n\n/**\n * Check if a container has one of the allowed field type classes\n */\nconst isAllowedFieldType = (container: HTMLElement): boolean => {\n return ALLOWED_FIELD_TYPES.some(\n (type) =>\n container.classList.contains(type) || container.classList.contains(`field-type-${type}`),\n )\n}\n\n/**\n * Resolve the .field-type container for a given event target\n * Only returns containers that match allowed field types\n */\nconst resolveContainerFromTarget = (target: EventTarget | null): HTMLElement | null => {\n if (!(target instanceof HTMLElement)) {\n return null\n }\n\n // Check for direct parent first\n let container = target.closest<HTMLElement>('.field-type')\n\n // If not found, fall back to React Select logic\n if (!container) {\n container = findContainerFromReactSelect(target)\n }\n\n // Only return if it's an allowed field type\n if (container && isAllowedFieldType(container)) {\n return container\n }\n\n return null\n}\n\n/**\n * Update the active container and toggle CSS class\n * - Avoids acting on disconnected nodes\n * - Avoids redundant class work\n */\nconst setActiveContainer = (next: HTMLElement | null): void => {\n // Normalize both references against disconnected nodes\n if (currentContainer && !currentContainer.isConnected) {\n currentContainer = null\n }\n if (next && !next.isConnected) {\n next = null\n }\n\n if (currentContainer === next) {\n return\n }\n\n currentContainer?.classList.remove('ai-plugin-active')\n if (next) {\n next.classList.add('ai-plugin-active')\n }\n currentContainer = next\n}\n\nconst clearActiveContainer = (): void => {\n if (currentContainer) {\n currentContainer.classList.remove('ai-plugin-active')\n currentContainer = null\n }\n\n // Cancel any pending RAF\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n}\n\nconst isInteractiveElement = (element: HTMLElement): boolean => {\n const tagName = element.tagName.toLowerCase()\n const interactiveTags = ['input', 'textarea', 'select', 'button']\n\n if (interactiveTags.includes(tagName)) {\n return true\n }\n\n // Check for contenteditable\n if (element.isContentEditable) {\n return true\n }\n\n // Check for elements with role=\"textbox\" or role=\"combobox\" (React Select)\n const role = element.getAttribute('role')\n if (role && ['combobox', 'listbox', 'searchbox', 'textbox'].includes(role)) {\n return true\n }\n\n return false\n}\n\n/**\n * Handle focus events - only activate if focus is on an interactive element within .field-type\n */\nconst onFocusIn = (e: FocusEvent): void => {\n const target = e.target\n if (!(target instanceof HTMLElement)) {\n return\n }\n\n // Early exit if we're already inside the current container\n if (currentContainer?.isConnected && currentContainer.contains(target)) {\n return\n }\n\n // Only activate if the focused element is actually interactive\n if (!isInteractiveElement(target)) {\n return\n }\n\n const container = resolveContainerFromTarget(target)\n setActiveContainer(container)\n}\n\n/**\n * Handle pointer/mouse events - only switch when clicking a different .field-type\n */\nconst onPointerDown = (e: PointerEvent): void => {\n const target = e.target\n if (!(target instanceof HTMLElement)) {\n return\n }\n\n // Early exit if clicking within current container\n if (currentContainer?.isConnected && currentContainer.contains(target)) {\n return\n }\n\n const container = resolveContainerFromTarget(target)\n setActiveContainer(container)\n}\n\n/**\n * Handle keyboard navigation (Tab key)\n */\nconst onKeyDown = (e: KeyboardEvent): void => {\n if (e.key !== 'Tab') {\n return\n }\n\n // Cancel any pending RAF to prevent queuing\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n }\n\n // Defer until after focus has shifted\n rafId = requestAnimationFrame(() => {\n rafId = null\n const container = resolveContainerFromTarget(document.activeElement)\n setActiveContainer(container)\n })\n}\n\n/**\n * Handle visibility changes to properly cleanup when page is hidden\n */\nconst onVisibilityChange = (): void => {\n if (typeof document !== 'undefined' && document.hidden) {\n // Clear active state and cancel pending operations\n clearActiveContainer()\n }\n}\n\n/**\n * Initialize document-level listeners to track the active field container.\n * When a container is active, it receives the 'ai-plugin-active' class.\n */\nexport const useActiveFieldTracking = (): void => {\n useEffect(() => {\n if (typeof window === 'undefined') {\n return\n }\n\n const pluginWindow = window as {\n __aiComposeTracking?: boolean\n __aiComposeTrackingController?: AbortController\n __aiComposeTrackingCount?: number\n } & Window\n\n // Track number of mounted users of the hook\n pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 0) + 1\n\n // Initialize listeners only once\n if (!pluginWindow.__aiComposeTracking) {\n const controller = new AbortController()\n pluginWindow.__aiComposeTrackingController = controller\n\n // Use capture for early handling\n document.addEventListener('focusin', onFocusIn, {\n capture: true,\n signal: controller.signal,\n })\n document.addEventListener('pointerdown', onPointerDown, {\n capture: true,\n passive: true,\n signal: controller.signal,\n })\n document.addEventListener('keydown', onKeyDown, {\n capture: true,\n signal: controller.signal,\n })\n document.addEventListener('visibilitychange', onVisibilityChange, {\n signal: controller.signal,\n })\n\n pluginWindow.__aiComposeTracking = true\n }\n\n return () => {\n // Decrement and cleanup when the last user unmounts\n pluginWindow.__aiComposeTrackingCount = (pluginWindow.__aiComposeTrackingCount ?? 1) - 1\n\n if ((pluginWindow.__aiComposeTrackingCount ?? 0) <= 0) {\n // Atomically remove all listeners\n pluginWindow.__aiComposeTrackingController?.abort()\n pluginWindow.__aiComposeTrackingController = undefined\n\n // Clear active state and cancel pending operations\n clearActiveContainer()\n\n // Reset all state\n pluginWindow.__aiComposeTracking = false\n pluginWindow.__aiComposeTrackingCount = 0\n }\n }\n }, [])\n}\n"],"names":["useEffect","ALLOWED_FIELD_TYPES","currentContainer","rafId","cssEscape","value","CSS","escape","replace","findContainerFromReactSelect","target","listbox","closest","id","selector","control","document","querySelector","isAllowedFieldType","container","some","type","classList","contains","resolveContainerFromTarget","HTMLElement","setActiveContainer","next","isConnected","remove","add","clearActiveContainer","cancelAnimationFrame","isInteractiveElement","element","tagName","toLowerCase","interactiveTags","includes","isContentEditable","role","getAttribute","onFocusIn","e","onPointerDown","onKeyDown","key","requestAnimationFrame","activeElement","onVisibilityChange","hidden","useActiveFieldTracking","window","pluginWindow","__aiComposeTrackingCount","__aiComposeTracking","controller","AbortController","__aiComposeTrackingController","addEventListener","capture","signal","passive","abort","undefined"],"mappings":"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"}
|