PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / components / layouts / DragResizeLayout.jsx

DragResizeLayout.jsx in Extendify 3.1.5, at src/Agent/components/layouts/DragResizeLayout.jsx

166 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useCanvasOpen } from '@agent/components/Canvas';
2 import { useDraggable } from '@agent/hooks/useDraggable';
3 import { usePortal } from '@agent/hooks/usePortal';
4 import { useResizable } from '@agent/hooks/useResizable';
5 import { useGlobalStore } from '@agent/state/global';
6 import { usePositionStore } from '@agent/state/position';
7 import {
8 createPortal,
9 useCallback,
10 useEffect,
11 useState,
12 } from '@wordpress/element';
13 import { __ } from '@wordpress/i18n';
14 import { close, Icon } from '@wordpress/icons';
15 import { AnimatePresence, motion } from 'framer-motion';
16
17 // TODO: some of the bound checking isn't working that well.
18 // For example, the user shrinks the height of the browser.
19 // On reload this should at least put the window in bounds.
20 // Maybe a resize observer would be useful.
21
22 export const DragResizeLayout = ({ children }) => {
23 const [el, setEl] = useState(null);
24 const mountNode = usePortal('extendify-agent-mount');
25 const { open, setOpen } = useGlobalStore();
26
27 // So it will re-render the hooks when mounted
28 const ref = useCallback(
29 (node) => requestAnimationFrame(() => setEl(node)),
30 [],
31 );
32 useDraggable({
33 el,
34 open,
35 initialPosition: usePositionStore.getState(),
36 onDragEnd: (x, y) => {
37 usePositionStore.getState().setPosition(x, y);
38 // External contract: no in-repo listener by design — emitted for
39 // host-page/analytics consumers (mirrors the consumed :resize-end below).
40 window.dispatchEvent(
41 new CustomEvent('extendify-agent:drag-end', { detail: { x, y } }),
42 );
43 },
44 });
45 useResizable({
46 el,
47 open,
48 initialSize: usePositionStore.getState(),
49 onResizeEnd: (width, height) => {
50 usePositionStore.setState({ width, height });
51 window.dispatchEvent(
52 new CustomEvent('extendify-agent:resize-end', {
53 detail: { width, height },
54 }),
55 );
56 },
57 });
58
59 useEffect(() => {
60 if (!el || !open) return;
61 // If it's not intersecting, close it
62 const observer = new IntersectionObserver((entries) => {
63 if (!entries[0].isIntersecting) setOpen(false);
64 });
65 observer.observe(el);
66 return () => observer.disconnect();
67 }, [el, open, setOpen]);
68
69 const canvasOpen = useCanvasOpen();
70
71 const closeAgent = () => {
72 setOpen(false);
73 // External contract: no in-repo listener by design — notifies
74 // host-page/analytics consumers the user dismissed the agent.
75 window.dispatchEvent(new CustomEvent('extendify-agent:closed-button'));
76 };
77
78 if (!mountNode || !open) return null;
79
80 return createPortal(
81 <AnimatePresence>
82 <motion.div
83 key="agent-popout-modal"
84 id="extendify-agent-popout-modal"
85 initial={{ opacity: 0 }}
86 animate={{ opacity: 1 }}
87 exit={{ y: 0, opacity: 0 }}
88 transition={{ duration: 0.4, delay: 0.1 }}
89 className="fixed bottom-0 right-0 z-higher flex max-h-full max-w-full flex-col rounded-lg border border-solid border-gray-300 bg-white shadow-2xl-flipped rtl:left-0 rtl:right-auto"
90 ref={ref}
91 >
92 <div className="group flex shrink-0 items-center justify-between overflow-hidden rounded-t-[calc(0.5rem-1px)] bg-banner-main text-banner-text">
93 <div
94 data-extendify-agent-handle
95 draggable
96 className="flex h-full grow cursor-grab active:cursor-grabbing select-none items-center justify-between gap-1 p-0 py-3"
97 >
98 <div className="flex h-5 px-4 max-w-36 overflow-hidden">
99 <img
100 className="max-h-full max-w-full object-contain"
101 src={window.extSharedData.partnerLogo}
102 alt={window.extSharedData.partnerName}
103 />
104 </div>
105 <div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
106 <DragButton />
107 </div>
108 </div>
109 {/* Only hidden: removing it shifts the drag handle to the edge. */}
110 <button
111 type="button"
112 className={`relative z-10 flex h-full items-center rounded-none border-0 bg-banner-main py-3 pe-4 ps-2 text-banner-text outline-hidden ring-design-main focus:shadow-none focus:outline-hidden focus-visible:outline-design-main ${canvasOpen ? 'invisible' : ''}`}
113 onClick={closeAgent}
114 >
115 <Icon
116 className="pointer-events-none fill-current leading-none"
117 icon={close}
118 size={18}
119 />
120 <span className="sr-only">
121 {__('Close window', 'extendify-local')}
122 </span>
123 </button>
124 </div>
125 {children}
126 <div
127 data-extendify-agent-resize
128 className="absolute -bottom-2 -right-2 z-high h-6 w-6 group"
129 >
130 <div className="h-6 w-6 cursor-se-resize group-active:cursor-nwse-resize" />
131 </div>
132 </motion.div>
133 </AnimatePresence>,
134 mountNode,
135 );
136 };
137
138 const DragButton = (props) => {
139 return (
140 <div style={{ userSelect: 'none' }} className="relative flex" {...props}>
141 <Icon
142 className="pointer-events-none text-banner-text"
143 icon={
144 <svg
145 xmlns="http://www.w3.org/2000/svg"
146 viewBox="0 0 24 24"
147 width="24"
148 height="24"
149 className="pointer-events-none"
150 aria-hidden="true"
151 focusable="false"
152 >
153 {/* hardcoded to use currentColor */}
154 <path
155 fill="currentColor"
156 d="M8 7h2V5H8v2zm0 6h2v-2H8v2zm0 6h2v-2H8v2zm6-14v2h2V5h-2zm0 8h2v-2h-2v2zm0 6h2v-2h-2v2z"
157 ></path>
158 </svg>
159 }
160 size={24}
161 />
162 <span className="sr-only">{__('Drag to move', 'extendify-local')}</span>
163 </div>
164 );
165 };
166