PluginProbe
Extendify / 3.1.3
Extendify v3.1.3
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.3, at src/Agent/components/layouts/DragResizeLayout.jsx

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