@bit.rhplus/draggable-modal 0.0.13 → 0.0.15
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/RESIZING_GUIDE.md +344 -0
- package/dist/RESIZING_GUIDE.md +344 -0
- package/dist/index.js +109 -16
- package/dist/index.js.map +1 -1
- package/dist/style.css +48 -0
- package/index.jsx +136 -27
- package/package.json +5 -5
- package/style.css +48 -0
- /package/dist/{preview-1770614312455.js → preview-1773314156693.js} +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# Návod: Resize modálního okna bez knihovny
|
|
2
|
+
|
|
3
|
+
Kompletní průvodce implementací resize (změna velikosti myší) na Ant Design 5 modálním okně v Reactu — bez použití jakékoliv resize knihovny.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Kontext
|
|
8
|
+
|
|
9
|
+
Komponenta `DraggableModal` rozšiřuje Ant Design `<Modal>` o drag (přetahování) pomocí `react-draggable`. Cílem bylo přidat i resize — změnu velikosti táhnutím za hrany/rohy.
|
|
10
|
+
|
|
11
|
+
**Důležité:** `react-draggable` je výhradně pro drag. Resize nenabízí. Musíme ho implementovat ručně.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Jak resize funguje — princip
|
|
16
|
+
|
|
17
|
+
Resize funguje na třech fázích mouse eventů:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
mousedown na handle → zapamatuj startovní pozici a rozměry
|
|
21
|
+
↓
|
|
22
|
+
mousemove na document → počítej delta, aktualizuj stav
|
|
23
|
+
↓
|
|
24
|
+
mouseup na document → ukonči resize, odregistruj listenery
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Klíč je přidávat `mousemove`/`mouseup` na **`document`**, ne na handle. Jinak by myš opuštění handle resize přerušila.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Krok 1: Stav pro rozměry
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const parseWidth = (w) => {
|
|
35
|
+
if (typeof w === 'number') return w;
|
|
36
|
+
if (typeof w === 'string') { const n = parseInt(w, 10); return isNaN(n) ? 520 : n; }
|
|
37
|
+
return 520;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const [modalSize, setModalSize] = React.useState({
|
|
41
|
+
width: parseWidth(props.width), // vždy číslo (ne "1400px")
|
|
42
|
+
height: null, // null = auto výška z obsahu
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Proč `parseWidth`?**
|
|
47
|
+
Ant Design přijímá `width` jako číslo i string (`"1400px"`). My potřebujeme číslo pro matematiku (`startW + dx`). Parsujeme hned při inicializaci.
|
|
48
|
+
|
|
49
|
+
**Proč `height: null`?**
|
|
50
|
+
Výchozí výška modalu je `auto` — určená obsahem. Výšku začínáme sledovat až při prvním resize (pak ji fixujeme na pixely).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Krok 2: Resize handles — průhledné aktivní zóny
|
|
55
|
+
|
|
56
|
+
Handles jsou prázdné `<div>` elementy bez viditelného obsahu. Stačí jim nastavit správný `cursor` a dostatečnou plochu pro klik.
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
.drm-handle {
|
|
60
|
+
position: absolute;
|
|
61
|
+
z-index: 10;
|
|
62
|
+
user-select: none;
|
|
63
|
+
pointer-events: auto; /* KRITICKÉ — viz sekce Nástrahy */
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Pravá hrana */
|
|
67
|
+
.drm-handle-e { top: 12px; right: -4px; width: 8px; height: calc(100% - 24px); cursor: e-resize; }
|
|
68
|
+
|
|
69
|
+
/* Levá hrana */
|
|
70
|
+
.drm-handle-w { top: 12px; left: -4px; width: 8px; height: calc(100% - 24px); cursor: w-resize; }
|
|
71
|
+
|
|
72
|
+
/* Spodní hrana */
|
|
73
|
+
.drm-handle-s { bottom: -4px; left: 12px; width: calc(100% - 24px); height: 8px; cursor: s-resize; }
|
|
74
|
+
|
|
75
|
+
/* Pravý dolní roh */
|
|
76
|
+
.drm-handle-se { bottom: -4px; right: -4px; width: 16px; height: 16px; cursor: se-resize; }
|
|
77
|
+
|
|
78
|
+
/* Levý dolní roh */
|
|
79
|
+
.drm-handle-sw { bottom: -4px; left: -4px; width: 16px; height: 16px; cursor: sw-resize; }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Handles jsou umístěny na `-4px` vně okna (polovina šířky 8px přesahuje). To vytváří přirozený aktivní prostor na samé hraně.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Krok 3: Logika resize
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const handleResizeStart = (e, direction) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopPropagation(); // zabrání spuštění drag při tahu za handle
|
|
92
|
+
|
|
93
|
+
// Změříme aktuální rozměry z DOM (ne ze stavu — stav může být zaostalý)
|
|
94
|
+
const modalEl = draggleRef.current?.querySelector('.ant-modal');
|
|
95
|
+
const contentEl = draggleRef.current?.querySelector('.ant-modal-content');
|
|
96
|
+
const startX = e.clientX;
|
|
97
|
+
const startY = e.clientY;
|
|
98
|
+
const startW = modalEl?.offsetWidth || modalSize.width;
|
|
99
|
+
const startH = contentEl?.offsetHeight || modalSize.height || 400;
|
|
100
|
+
|
|
101
|
+
const onMouseMove = (moveEvent) => {
|
|
102
|
+
const dx = moveEvent.clientX - startX; // kolik px jsme se posunuli
|
|
103
|
+
const dy = moveEvent.clientY - startY;
|
|
104
|
+
|
|
105
|
+
setModalSize(prev => {
|
|
106
|
+
let newW = prev.width;
|
|
107
|
+
let newH = prev.height !== null ? prev.height : startH;
|
|
108
|
+
|
|
109
|
+
if (direction.includes('e')) newW = Math.max(300, startW + dx); // tah doprava
|
|
110
|
+
if (direction.includes('w')) newW = Math.max(300, startW - dx); // tah doleva
|
|
111
|
+
if (direction.includes('s')) newH = Math.max(200, startH + dy); // tah dolů
|
|
112
|
+
|
|
113
|
+
return { width: newW, height: newH };
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onMouseUp = () => {
|
|
118
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
119
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// DŮLEŽITÉ: listenery na document, ne na handle
|
|
123
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
124
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Proč čteme z DOM a ne ze stavu?**
|
|
129
|
+
Při zahájení resize chceme **aktuální vizuální rozměry**. Stav `modalSize` může být zaostalý o render cyklus. `offsetWidth`/`offsetHeight` vrací vždy aktuální hodnotu z DOM.
|
|
130
|
+
|
|
131
|
+
**Minimální rozměry:**
|
|
132
|
+
`Math.max(300, ...)` a `Math.max(200, ...)` zabrání zborcení layoutu.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Krok 4: Aplikace rozměrů na modal
|
|
137
|
+
|
|
138
|
+
```jsx
|
|
139
|
+
<Modal
|
|
140
|
+
{...props}
|
|
141
|
+
width={resizable ? modalSize.width : props.width}
|
|
142
|
+
styles={resolveStyles()}
|
|
143
|
+
>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const resolveStyles = () => {
|
|
148
|
+
if (resizable && modalSize.height !== null) {
|
|
149
|
+
return {
|
|
150
|
+
body: {
|
|
151
|
+
paddingBottom: '30px',
|
|
152
|
+
height: `calc(${modalSize.height}px - 110px)`, // odečteme header + footer
|
|
153
|
+
overflow: 'auto',
|
|
154
|
+
display: 'flex',
|
|
155
|
+
flexDirection: 'column',
|
|
156
|
+
},
|
|
157
|
+
content: {
|
|
158
|
+
height: `${modalSize.height}px`,
|
|
159
|
+
display: 'flex',
|
|
160
|
+
flexDirection: 'column',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ... fallback pro statický height prop
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`110px` je odhadovaná výška headeru + footeru. `overflow: auto` na body zajistí scrollování při přetečení obsahu.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Krok 5: Kam umístit handles — nástraha Ant Design
|
|
173
|
+
|
|
174
|
+
Naivní přístup: vložit handles přímo do `modalRender` wrapperu:
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
// ❌ NEFUNGUJE
|
|
178
|
+
modalRender={(modal) => (
|
|
179
|
+
<Draggable>
|
|
180
|
+
<div ref={draggleRef}>
|
|
181
|
+
{modal}
|
|
182
|
+
{resizeHandles} {/* <-- tady NE */}
|
|
183
|
+
</div>
|
|
184
|
+
</Draggable>
|
|
185
|
+
)}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Proč ne?** Wrapper div je block-level element — má šířku `100% viewportu`. Handles s `position: absolute; right: -4px` se zobrazí na okraji viewportu, ne modálního okna. Uživatel je nevidí.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
.ant-modal-wrap (100vw)
|
|
192
|
+
[draggleRef div = 100vw] ← handles jsou tady → špatně
|
|
193
|
+
.ant-modal (1400px, margin:auto)
|
|
194
|
+
.ant-modal-content
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Správné řešení: ReactDOM.createPortal do `.ant-modal-content`
|
|
198
|
+
|
|
199
|
+
```jsx
|
|
200
|
+
// ✅ SPRÁVNĚ
|
|
201
|
+
{resizable && antModalEl && ReactDOM.createPortal(resizeHandles, antModalEl)}
|
|
202
|
+
// antModalEl = element .ant-modal-content
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`.ant-modal-content` má `position: relative` → handles s `position: absolute` jsou relativní k němu. Handles jsou přesně na hranách viditelného okna.
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
.ant-modal-content (position: relative) ← portal target
|
|
209
|
+
.ant-modal-header
|
|
210
|
+
.ant-modal-body
|
|
211
|
+
.ant-modal-footer
|
|
212
|
+
[drm-handle-e] ← position: absolute, right: -4px ✓
|
|
213
|
+
[drm-handle-se] ← position: absolute, bottom: -4px, right: -4px ✓
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Krok 6: Detekce `.ant-modal-content` elementu
|
|
219
|
+
|
|
220
|
+
Portal cíl musíme najít v DOM poté, co Ant Design modal narenduje. Použijeme `useLayoutEffect` — běží synchronně po DOM mutaci:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const [antModalEl, setAntModalEl] = React.useState(null);
|
|
224
|
+
|
|
225
|
+
React.useLayoutEffect(() => {
|
|
226
|
+
if (resizable && open && draggleRef.current) {
|
|
227
|
+
setAntModalEl(draggleRef.current.querySelector('.ant-modal-content'));
|
|
228
|
+
} else {
|
|
229
|
+
setAntModalEl(null);
|
|
230
|
+
}
|
|
231
|
+
}, [resizable, open]);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Proč `useLayoutEffect` a ne `useEffect`?**
|
|
235
|
+
`useEffect` běží asynchronně po paint — uživatel by na první render viděl modal bez handles. `useLayoutEffect` běží synchronně před paint, takže handles se objeví ihned s modalem (druhý render, ale bez vizuálního bliknutí).
|
|
236
|
+
|
|
237
|
+
**Tok na první otevření:**
|
|
238
|
+
1. `open` změní na `true` → první render → `antModalEl` je `null` → handles se nevykreslí
|
|
239
|
+
2. `useLayoutEffect` najde `.ant-modal-content`, zavolá `setAntModalEl`
|
|
240
|
+
3. Druhý render → handles se vykreslí přes portal
|
|
241
|
+
4. Vše synchronní — uživatel nevidí blikání
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Nástraha: `pointer-events: none` na `.ant-modal`
|
|
246
|
+
|
|
247
|
+
Ant Design 5 nastavuje na `.ant-modal`:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// node_modules/antd/es/modal/style/index.js
|
|
251
|
+
'.ant-modal': {
|
|
252
|
+
pointerEvents: 'none', // ← zabraňuje klikům mimo .ant-modal-content
|
|
253
|
+
}
|
|
254
|
+
'.ant-modal-content': {
|
|
255
|
+
pointerEvents: 'auto', // ← re-aktivuje pro obsah
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Pokud bychom portálovali handles do `.ant-modal` (ne do `.ant-modal-content`), handles by dědily `pointer-events: none` → `onMouseDown` by se nikdy nespustil.
|
|
260
|
+
|
|
261
|
+
Proto portálujeme do `.ant-modal-content` (má `pointer-events: auto`) + přidáme `pointer-events: auto` na `.drm-handle` jako pojistku.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Nástraha: `e.stopPropagation()` na resize handle
|
|
266
|
+
|
|
267
|
+
Draggable wrapper obaluje celý modal. Bez `stopPropagation` by `mousedown` na resize handle také spustil drag. Výsledek: modal se táhne místo resize.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const handleResizeStart = (e, direction) => {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
e.stopPropagation(); // zabrání bubblingu k Draggable
|
|
273
|
+
// ...
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Výsledný strom v DOM
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
[draggleRef div] ← Draggable wrapper (full-width)
|
|
283
|
+
.ant-modal ← width prop → kontroluje vizuální šířku
|
|
284
|
+
.ant-modal-content ← portal target (position: relative)
|
|
285
|
+
.ant-modal-header
|
|
286
|
+
.ant-modal-body
|
|
287
|
+
.ant-modal-footer
|
|
288
|
+
.drm-handle-e ← right: -4px, výška okna
|
|
289
|
+
.drm-handle-w ← left: -4px, výška okna
|
|
290
|
+
.drm-handle-s ← bottom: -4px, šířka okna
|
|
291
|
+
.drm-handle-se ← bottom: -4px, right: -4px
|
|
292
|
+
.drm-handle-sw ← bottom: -4px, left: -4px
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Kompletní flow resize
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
Uživatel → mousedown na .drm-handle-se
|
|
301
|
+
↓
|
|
302
|
+
handleResizeStart('se')
|
|
303
|
+
- e.stopPropagation() → Draggable nezačne drag
|
|
304
|
+
- uloží: startX, startY, startW (.ant-modal.offsetWidth), startH (.ant-modal-content.offsetHeight)
|
|
305
|
+
- registruje: document.onmousemove, document.onmouseup
|
|
306
|
+
↓
|
|
307
|
+
Uživatel táhne myší
|
|
308
|
+
↓
|
|
309
|
+
onMouseMove
|
|
310
|
+
- dx = clientX - startX, dy = clientY - startY
|
|
311
|
+
- 'se' → newW = max(300, startW + dx), newH = max(200, startH + dy)
|
|
312
|
+
- setModalSize({ width: newW, height: newH })
|
|
313
|
+
↓
|
|
314
|
+
React re-render
|
|
315
|
+
- <Modal width={newW}> → .ant-modal dostane novou šířku
|
|
316
|
+
- styles.content.height = newH → .ant-modal-content dostane novou výšku
|
|
317
|
+
- handles v portalu se přesunou s .ant-modal-content
|
|
318
|
+
↓
|
|
319
|
+
Uživatel pustí myš
|
|
320
|
+
↓
|
|
321
|
+
onMouseUp → removeEventListener (cleanup)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Použití
|
|
327
|
+
|
|
328
|
+
```jsx
|
|
329
|
+
// Resize zapnut (default)
|
|
330
|
+
<DraggableModal open={open} onCancel={onClose} title="Okno" width={800}>
|
|
331
|
+
obsah
|
|
332
|
+
</DraggableModal>
|
|
333
|
+
|
|
334
|
+
// Resize vypnut
|
|
335
|
+
<DraggableModal resizable={false} open={open} onCancel={onClose} title="Okno">
|
|
336
|
+
obsah
|
|
337
|
+
</DraggableModal>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Proč ne knihovna (re-resizable, react-resizable)?
|
|
343
|
+
|
|
344
|
+
Obě fungují na principu wrapperu kolem elementu. Pro Ant Design Modal je problém, že wrapper musí obalit `.ant-modal`, ale `.ant-modal` je renderován interně knihovnou — nemáme nad ním přímou kontrolu z JSX. Ruční implementace pomocí mouse eventů je přímočařejší a nevyžaduje nové závislosti.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# Návod: Resize modálního okna bez knihovny
|
|
2
|
+
|
|
3
|
+
Kompletní průvodce implementací resize (změna velikosti myší) na Ant Design 5 modálním okně v Reactu — bez použití jakékoliv resize knihovny.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Kontext
|
|
8
|
+
|
|
9
|
+
Komponenta `DraggableModal` rozšiřuje Ant Design `<Modal>` o drag (přetahování) pomocí `react-draggable`. Cílem bylo přidat i resize — změnu velikosti táhnutím za hrany/rohy.
|
|
10
|
+
|
|
11
|
+
**Důležité:** `react-draggable` je výhradně pro drag. Resize nenabízí. Musíme ho implementovat ručně.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Jak resize funguje — princip
|
|
16
|
+
|
|
17
|
+
Resize funguje na třech fázích mouse eventů:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
mousedown na handle → zapamatuj startovní pozici a rozměry
|
|
21
|
+
↓
|
|
22
|
+
mousemove na document → počítej delta, aktualizuj stav
|
|
23
|
+
↓
|
|
24
|
+
mouseup na document → ukonči resize, odregistruj listenery
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Klíč je přidávat `mousemove`/`mouseup` na **`document`**, ne na handle. Jinak by myš opuštění handle resize přerušila.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Krok 1: Stav pro rozměry
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const parseWidth = (w) => {
|
|
35
|
+
if (typeof w === 'number') return w;
|
|
36
|
+
if (typeof w === 'string') { const n = parseInt(w, 10); return isNaN(n) ? 520 : n; }
|
|
37
|
+
return 520;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const [modalSize, setModalSize] = React.useState({
|
|
41
|
+
width: parseWidth(props.width), // vždy číslo (ne "1400px")
|
|
42
|
+
height: null, // null = auto výška z obsahu
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Proč `parseWidth`?**
|
|
47
|
+
Ant Design přijímá `width` jako číslo i string (`"1400px"`). My potřebujeme číslo pro matematiku (`startW + dx`). Parsujeme hned při inicializaci.
|
|
48
|
+
|
|
49
|
+
**Proč `height: null`?**
|
|
50
|
+
Výchozí výška modalu je `auto` — určená obsahem. Výšku začínáme sledovat až při prvním resize (pak ji fixujeme na pixely).
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Krok 2: Resize handles — průhledné aktivní zóny
|
|
55
|
+
|
|
56
|
+
Handles jsou prázdné `<div>` elementy bez viditelného obsahu. Stačí jim nastavit správný `cursor` a dostatečnou plochu pro klik.
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
.drm-handle {
|
|
60
|
+
position: absolute;
|
|
61
|
+
z-index: 10;
|
|
62
|
+
user-select: none;
|
|
63
|
+
pointer-events: auto; /* KRITICKÉ — viz sekce Nástrahy */
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Pravá hrana */
|
|
67
|
+
.drm-handle-e { top: 12px; right: -4px; width: 8px; height: calc(100% - 24px); cursor: e-resize; }
|
|
68
|
+
|
|
69
|
+
/* Levá hrana */
|
|
70
|
+
.drm-handle-w { top: 12px; left: -4px; width: 8px; height: calc(100% - 24px); cursor: w-resize; }
|
|
71
|
+
|
|
72
|
+
/* Spodní hrana */
|
|
73
|
+
.drm-handle-s { bottom: -4px; left: 12px; width: calc(100% - 24px); height: 8px; cursor: s-resize; }
|
|
74
|
+
|
|
75
|
+
/* Pravý dolní roh */
|
|
76
|
+
.drm-handle-se { bottom: -4px; right: -4px; width: 16px; height: 16px; cursor: se-resize; }
|
|
77
|
+
|
|
78
|
+
/* Levý dolní roh */
|
|
79
|
+
.drm-handle-sw { bottom: -4px; left: -4px; width: 16px; height: 16px; cursor: sw-resize; }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Handles jsou umístěny na `-4px` vně okna (polovina šířky 8px přesahuje). To vytváří přirozený aktivní prostor na samé hraně.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Krok 3: Logika resize
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const handleResizeStart = (e, direction) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopPropagation(); // zabrání spuštění drag při tahu za handle
|
|
92
|
+
|
|
93
|
+
// Změříme aktuální rozměry z DOM (ne ze stavu — stav může být zaostalý)
|
|
94
|
+
const modalEl = draggleRef.current?.querySelector('.ant-modal');
|
|
95
|
+
const contentEl = draggleRef.current?.querySelector('.ant-modal-content');
|
|
96
|
+
const startX = e.clientX;
|
|
97
|
+
const startY = e.clientY;
|
|
98
|
+
const startW = modalEl?.offsetWidth || modalSize.width;
|
|
99
|
+
const startH = contentEl?.offsetHeight || modalSize.height || 400;
|
|
100
|
+
|
|
101
|
+
const onMouseMove = (moveEvent) => {
|
|
102
|
+
const dx = moveEvent.clientX - startX; // kolik px jsme se posunuli
|
|
103
|
+
const dy = moveEvent.clientY - startY;
|
|
104
|
+
|
|
105
|
+
setModalSize(prev => {
|
|
106
|
+
let newW = prev.width;
|
|
107
|
+
let newH = prev.height !== null ? prev.height : startH;
|
|
108
|
+
|
|
109
|
+
if (direction.includes('e')) newW = Math.max(300, startW + dx); // tah doprava
|
|
110
|
+
if (direction.includes('w')) newW = Math.max(300, startW - dx); // tah doleva
|
|
111
|
+
if (direction.includes('s')) newH = Math.max(200, startH + dy); // tah dolů
|
|
112
|
+
|
|
113
|
+
return { width: newW, height: newH };
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const onMouseUp = () => {
|
|
118
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
119
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// DŮLEŽITÉ: listenery na document, ne na handle
|
|
123
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
124
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Proč čteme z DOM a ne ze stavu?**
|
|
129
|
+
Při zahájení resize chceme **aktuální vizuální rozměry**. Stav `modalSize` může být zaostalý o render cyklus. `offsetWidth`/`offsetHeight` vrací vždy aktuální hodnotu z DOM.
|
|
130
|
+
|
|
131
|
+
**Minimální rozměry:**
|
|
132
|
+
`Math.max(300, ...)` a `Math.max(200, ...)` zabrání zborcení layoutu.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Krok 4: Aplikace rozměrů na modal
|
|
137
|
+
|
|
138
|
+
```jsx
|
|
139
|
+
<Modal
|
|
140
|
+
{...props}
|
|
141
|
+
width={resizable ? modalSize.width : props.width}
|
|
142
|
+
styles={resolveStyles()}
|
|
143
|
+
>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const resolveStyles = () => {
|
|
148
|
+
if (resizable && modalSize.height !== null) {
|
|
149
|
+
return {
|
|
150
|
+
body: {
|
|
151
|
+
paddingBottom: '30px',
|
|
152
|
+
height: `calc(${modalSize.height}px - 110px)`, // odečteme header + footer
|
|
153
|
+
overflow: 'auto',
|
|
154
|
+
display: 'flex',
|
|
155
|
+
flexDirection: 'column',
|
|
156
|
+
},
|
|
157
|
+
content: {
|
|
158
|
+
height: `${modalSize.height}px`,
|
|
159
|
+
display: 'flex',
|
|
160
|
+
flexDirection: 'column',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ... fallback pro statický height prop
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`110px` je odhadovaná výška headeru + footeru. `overflow: auto` na body zajistí scrollování při přetečení obsahu.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Krok 5: Kam umístit handles — nástraha Ant Design
|
|
173
|
+
|
|
174
|
+
Naivní přístup: vložit handles přímo do `modalRender` wrapperu:
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
// ❌ NEFUNGUJE
|
|
178
|
+
modalRender={(modal) => (
|
|
179
|
+
<Draggable>
|
|
180
|
+
<div ref={draggleRef}>
|
|
181
|
+
{modal}
|
|
182
|
+
{resizeHandles} {/* <-- tady NE */}
|
|
183
|
+
</div>
|
|
184
|
+
</Draggable>
|
|
185
|
+
)}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Proč ne?** Wrapper div je block-level element — má šířku `100% viewportu`. Handles s `position: absolute; right: -4px` se zobrazí na okraji viewportu, ne modálního okna. Uživatel je nevidí.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
.ant-modal-wrap (100vw)
|
|
192
|
+
[draggleRef div = 100vw] ← handles jsou tady → špatně
|
|
193
|
+
.ant-modal (1400px, margin:auto)
|
|
194
|
+
.ant-modal-content
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Správné řešení: ReactDOM.createPortal do `.ant-modal-content`
|
|
198
|
+
|
|
199
|
+
```jsx
|
|
200
|
+
// ✅ SPRÁVNĚ
|
|
201
|
+
{resizable && antModalEl && ReactDOM.createPortal(resizeHandles, antModalEl)}
|
|
202
|
+
// antModalEl = element .ant-modal-content
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`.ant-modal-content` má `position: relative` → handles s `position: absolute` jsou relativní k němu. Handles jsou přesně na hranách viditelného okna.
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
.ant-modal-content (position: relative) ← portal target
|
|
209
|
+
.ant-modal-header
|
|
210
|
+
.ant-modal-body
|
|
211
|
+
.ant-modal-footer
|
|
212
|
+
[drm-handle-e] ← position: absolute, right: -4px ✓
|
|
213
|
+
[drm-handle-se] ← position: absolute, bottom: -4px, right: -4px ✓
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Krok 6: Detekce `.ant-modal-content` elementu
|
|
219
|
+
|
|
220
|
+
Portal cíl musíme najít v DOM poté, co Ant Design modal narenduje. Použijeme `useLayoutEffect` — běží synchronně po DOM mutaci:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const [antModalEl, setAntModalEl] = React.useState(null);
|
|
224
|
+
|
|
225
|
+
React.useLayoutEffect(() => {
|
|
226
|
+
if (resizable && open && draggleRef.current) {
|
|
227
|
+
setAntModalEl(draggleRef.current.querySelector('.ant-modal-content'));
|
|
228
|
+
} else {
|
|
229
|
+
setAntModalEl(null);
|
|
230
|
+
}
|
|
231
|
+
}, [resizable, open]);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Proč `useLayoutEffect` a ne `useEffect`?**
|
|
235
|
+
`useEffect` běží asynchronně po paint — uživatel by na první render viděl modal bez handles. `useLayoutEffect` běží synchronně před paint, takže handles se objeví ihned s modalem (druhý render, ale bez vizuálního bliknutí).
|
|
236
|
+
|
|
237
|
+
**Tok na první otevření:**
|
|
238
|
+
1. `open` změní na `true` → první render → `antModalEl` je `null` → handles se nevykreslí
|
|
239
|
+
2. `useLayoutEffect` najde `.ant-modal-content`, zavolá `setAntModalEl`
|
|
240
|
+
3. Druhý render → handles se vykreslí přes portal
|
|
241
|
+
4. Vše synchronní — uživatel nevidí blikání
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Nástraha: `pointer-events: none` na `.ant-modal`
|
|
246
|
+
|
|
247
|
+
Ant Design 5 nastavuje na `.ant-modal`:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// node_modules/antd/es/modal/style/index.js
|
|
251
|
+
'.ant-modal': {
|
|
252
|
+
pointerEvents: 'none', // ← zabraňuje klikům mimo .ant-modal-content
|
|
253
|
+
}
|
|
254
|
+
'.ant-modal-content': {
|
|
255
|
+
pointerEvents: 'auto', // ← re-aktivuje pro obsah
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Pokud bychom portálovali handles do `.ant-modal` (ne do `.ant-modal-content`), handles by dědily `pointer-events: none` → `onMouseDown` by se nikdy nespustil.
|
|
260
|
+
|
|
261
|
+
Proto portálujeme do `.ant-modal-content` (má `pointer-events: auto`) + přidáme `pointer-events: auto` na `.drm-handle` jako pojistku.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Nástraha: `e.stopPropagation()` na resize handle
|
|
266
|
+
|
|
267
|
+
Draggable wrapper obaluje celý modal. Bez `stopPropagation` by `mousedown` na resize handle také spustil drag. Výsledek: modal se táhne místo resize.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const handleResizeStart = (e, direction) => {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
e.stopPropagation(); // zabrání bubblingu k Draggable
|
|
273
|
+
// ...
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Výsledný strom v DOM
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
[draggleRef div] ← Draggable wrapper (full-width)
|
|
283
|
+
.ant-modal ← width prop → kontroluje vizuální šířku
|
|
284
|
+
.ant-modal-content ← portal target (position: relative)
|
|
285
|
+
.ant-modal-header
|
|
286
|
+
.ant-modal-body
|
|
287
|
+
.ant-modal-footer
|
|
288
|
+
.drm-handle-e ← right: -4px, výška okna
|
|
289
|
+
.drm-handle-w ← left: -4px, výška okna
|
|
290
|
+
.drm-handle-s ← bottom: -4px, šířka okna
|
|
291
|
+
.drm-handle-se ← bottom: -4px, right: -4px
|
|
292
|
+
.drm-handle-sw ← bottom: -4px, left: -4px
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Kompletní flow resize
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
Uživatel → mousedown na .drm-handle-se
|
|
301
|
+
↓
|
|
302
|
+
handleResizeStart('se')
|
|
303
|
+
- e.stopPropagation() → Draggable nezačne drag
|
|
304
|
+
- uloží: startX, startY, startW (.ant-modal.offsetWidth), startH (.ant-modal-content.offsetHeight)
|
|
305
|
+
- registruje: document.onmousemove, document.onmouseup
|
|
306
|
+
↓
|
|
307
|
+
Uživatel táhne myší
|
|
308
|
+
↓
|
|
309
|
+
onMouseMove
|
|
310
|
+
- dx = clientX - startX, dy = clientY - startY
|
|
311
|
+
- 'se' → newW = max(300, startW + dx), newH = max(200, startH + dy)
|
|
312
|
+
- setModalSize({ width: newW, height: newH })
|
|
313
|
+
↓
|
|
314
|
+
React re-render
|
|
315
|
+
- <Modal width={newW}> → .ant-modal dostane novou šířku
|
|
316
|
+
- styles.content.height = newH → .ant-modal-content dostane novou výšku
|
|
317
|
+
- handles v portalu se přesunou s .ant-modal-content
|
|
318
|
+
↓
|
|
319
|
+
Uživatel pustí myš
|
|
320
|
+
↓
|
|
321
|
+
onMouseUp → removeEventListener (cleanup)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Použití
|
|
327
|
+
|
|
328
|
+
```jsx
|
|
329
|
+
// Resize zapnut (default)
|
|
330
|
+
<DraggableModal open={open} onCancel={onClose} title="Okno" width={800}>
|
|
331
|
+
obsah
|
|
332
|
+
</DraggableModal>
|
|
333
|
+
|
|
334
|
+
// Resize vypnut
|
|
335
|
+
<DraggableModal resizable={false} open={open} onCancel={onClose} title="Okno">
|
|
336
|
+
obsah
|
|
337
|
+
</DraggableModal>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Proč ne knihovna (re-resizable, react-resizable)?
|
|
343
|
+
|
|
344
|
+
Obě fungují na principu wrapperu kolem elementu. Pro Ant Design Modal je problém, že wrapper musí obalit `.ant-modal`, ale `.ant-modal` je renderován interně knihovnou — nemáme nad ním přímou kontrolu z JSX. Ruční implementace pomocí mouse eventů je přímočařejší a nevyžaduje nové závislosti.
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
|
-
import { Fragment as _Fragment,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { default as Modal } from 'antd/es/modal';
|
|
5
5
|
import Draggable from 'react-draggable';
|
|
6
6
|
import ReactDOM from 'react-dom';
|
|
7
7
|
import './style.css';
|
|
8
|
+
const parseWidth = (w) => {
|
|
9
|
+
if (typeof w === 'number')
|
|
10
|
+
return w;
|
|
11
|
+
if (typeof w === 'string') {
|
|
12
|
+
const n = parseInt(w, 10);
|
|
13
|
+
return isNaN(n) ? 520 : n;
|
|
14
|
+
}
|
|
15
|
+
return 520;
|
|
16
|
+
};
|
|
17
|
+
// Wrapper – podmíněné renderování = destroy on close (kompletní unmount při zavření)
|
|
8
18
|
const DraggableModal = (props) => {
|
|
9
|
-
|
|
19
|
+
if (!props.open)
|
|
20
|
+
return null;
|
|
21
|
+
return _jsx(DraggableModalInner, { ...props });
|
|
22
|
+
};
|
|
23
|
+
// Inner komponenta – obsahuje veškerou logiku a stav
|
|
24
|
+
// Při zavření se kompletně unmountuje → všechny hooks a stav jsou zničeny
|
|
25
|
+
// Při otevření se vytvoří čerstvá instance → žádný starý stav
|
|
26
|
+
const DraggableModalInner = (props) => {
|
|
27
|
+
const { open, onOk, onCancel, isInteractive = false, title, children, getContainer, height, resizable = true } = props;
|
|
10
28
|
const draggleRef = React.useRef(null);
|
|
11
29
|
const [disabled, setDisabled] = React.useState(true);
|
|
12
30
|
const [bounds, setBounds] = React.useState({
|
|
@@ -15,11 +33,25 @@ const DraggableModal = (props) => {
|
|
|
15
33
|
bottom: 0,
|
|
16
34
|
right: 0,
|
|
17
35
|
});
|
|
36
|
+
const [modalSize, setModalSize] = React.useState({
|
|
37
|
+
width: parseWidth(props.width),
|
|
38
|
+
height: null,
|
|
39
|
+
});
|
|
40
|
+
const [antModalEl, setAntModalEl] = React.useState(null);
|
|
41
|
+
const [dragPos, setDragPos] = React.useState({ x: 0, y: 0 });
|
|
18
42
|
React.useEffect(() => {
|
|
19
43
|
if (isInteractive) {
|
|
20
44
|
import('./interactiveStyle.css');
|
|
21
45
|
}
|
|
22
46
|
}, [isInteractive]);
|
|
47
|
+
React.useLayoutEffect(() => {
|
|
48
|
+
if (resizable && open && draggleRef.current) {
|
|
49
|
+
setAntModalEl(draggleRef.current.querySelector('.ant-modal-content'));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
setAntModalEl(null);
|
|
53
|
+
}
|
|
54
|
+
}, [resizable, open]);
|
|
23
55
|
const onStart = (_event, uiData) => {
|
|
24
56
|
const { clientWidth, clientHeight } = window.document.documentElement;
|
|
25
57
|
const targetRect = draggleRef.current?.getBoundingClientRect();
|
|
@@ -33,32 +65,93 @@ const DraggableModal = (props) => {
|
|
|
33
65
|
bottom: clientHeight - (targetRect.bottom - uiData.y),
|
|
34
66
|
});
|
|
35
67
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
const handleResizeStart = (e, direction) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
const modalEl = draggleRef.current?.querySelector('.ant-modal');
|
|
72
|
+
const contentEl = draggleRef.current?.querySelector('.ant-modal-content');
|
|
73
|
+
const startX = e.clientX;
|
|
74
|
+
const startY = e.clientY;
|
|
75
|
+
const startW = modalEl?.offsetWidth || modalSize.width;
|
|
76
|
+
const startH = contentEl?.offsetHeight || modalSize.height || 400;
|
|
77
|
+
const startDragX = dragPos.x;
|
|
78
|
+
const onMouseMove = (moveEvent) => {
|
|
79
|
+
const dx = moveEvent.clientX - startX;
|
|
80
|
+
const dy = moveEvent.clientY - startY;
|
|
81
|
+
setModalSize(prev => {
|
|
82
|
+
let newW = prev.width;
|
|
83
|
+
let newH = prev.height !== null ? prev.height : startH;
|
|
84
|
+
if (direction.includes('e'))
|
|
85
|
+
newW = Math.max(300, startW + dx);
|
|
86
|
+
if (direction.includes('w'))
|
|
87
|
+
newW = Math.max(300, startW - dx);
|
|
88
|
+
if (direction.includes('s'))
|
|
89
|
+
newH = Math.max(200, startH + dy);
|
|
90
|
+
return { width: newW, height: newH };
|
|
91
|
+
});
|
|
92
|
+
// Kompenzace: Ant Design centruje modal přes margin:auto.
|
|
93
|
+
// Při změně width se levý okraj posune o dx/2 — opravíme pozici Draggable.
|
|
94
|
+
if (direction.includes('e') || direction.includes('w')) {
|
|
95
|
+
setDragPos(prev => ({ ...prev, x: startDragX + dx / 2 }));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const onMouseUp = () => {
|
|
99
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
100
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
101
|
+
};
|
|
102
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
103
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
104
|
+
};
|
|
105
|
+
const resolveStyles = () => {
|
|
106
|
+
if (resizable && modalSize.height !== null) {
|
|
107
|
+
return {
|
|
108
|
+
body: {
|
|
109
|
+
paddingBottom: '30px',
|
|
110
|
+
height: `calc(${modalSize.height}px - 110px)`,
|
|
111
|
+
overflow: 'auto',
|
|
112
|
+
display: 'flex',
|
|
113
|
+
flexDirection: 'column',
|
|
114
|
+
boxSizing: 'border-box',
|
|
115
|
+
},
|
|
116
|
+
content: {
|
|
117
|
+
height: `${modalSize.height}px`,
|
|
118
|
+
display: 'flex',
|
|
119
|
+
flexDirection: 'column',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
44
124
|
body: {
|
|
45
125
|
paddingBottom: '30px',
|
|
46
126
|
...(height && {
|
|
47
127
|
height: `calc(${height} - 110px)`,
|
|
48
128
|
overflow: 'hidden',
|
|
49
129
|
display: 'flex',
|
|
50
|
-
flexDirection: 'column'
|
|
51
|
-
|
|
130
|
+
flexDirection: 'column',
|
|
131
|
+
boxSizing: 'border-box',
|
|
132
|
+
}),
|
|
52
133
|
},
|
|
53
134
|
content: height ? {
|
|
54
135
|
height: height,
|
|
55
136
|
display: 'flex',
|
|
56
|
-
flexDirection: 'column'
|
|
57
|
-
} : {}
|
|
58
|
-
}
|
|
137
|
+
flexDirection: 'column',
|
|
138
|
+
} : {},
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const resizeHandles = (_jsxs(_Fragment, { children: [_jsx("div", { className: "drm-handle drm-handle-e", onMouseDown: (e) => handleResizeStart(e, 'e') }), _jsx("div", { className: "drm-handle drm-handle-w", onMouseDown: (e) => handleResizeStart(e, 'w') }), _jsx("div", { className: "drm-handle drm-handle-s", onMouseDown: (e) => handleResizeStart(e, 's') }), _jsx("div", { className: "drm-handle drm-handle-se", onMouseDown: (e) => handleResizeStart(e, 'se') }), _jsx("div", { className: "drm-handle drm-handle-sw", onMouseDown: (e) => handleResizeStart(e, 'sw') })] }));
|
|
142
|
+
// Sdílený Modal komponent
|
|
143
|
+
const modalContent = (_jsx(Modal, { ...props, mask: !isInteractive, getContainer: getContainer || false, zIndex: props.zIndex || 99999999, width: resizable ? modalSize.width : props.width, title: _jsx("div", { style: { width: '100%', cursor: 'move' }, onMouseOver: () => {
|
|
144
|
+
if (disabled) {
|
|
145
|
+
setDisabled(false);
|
|
146
|
+
}
|
|
147
|
+
}, onMouseOut: () => {
|
|
148
|
+
setDisabled(true);
|
|
149
|
+
}, onFocus: () => { }, onBlur: () => { }, children: title }), open: open, onOk: onOk, onCancel: onCancel, styles: resolveStyles(), modalRender: (modal) => (_jsx(Draggable, { disabled: disabled, bounds: bounds, position: dragPos, onDrag: (e, data) => setDragPos({ x: data.x, y: data.y }), onStart: (event, uiData) => onStart(event, uiData), children: _jsx("div", { ref: draggleRef, style: { position: 'relative' }, children: modal }) })), children: _jsx(_Fragment, { children: children }) }));
|
|
59
150
|
// Pokud je getContainer funkce, použijeme portal
|
|
60
151
|
// Jinak renderujeme přímo (zachová React context)
|
|
61
|
-
return
|
|
152
|
+
return (_jsxs(_Fragment, { children: [typeof getContainer === 'function'
|
|
153
|
+
? ReactDOM.createPortal(modalContent, getContainer())
|
|
154
|
+
: modalContent, resizable && antModalEl && ReactDOM.createPortal(resizeHandles, antModalEl)] }));
|
|
62
155
|
};
|
|
63
156
|
export default DraggableModal;
|
|
64
157
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.jsx"],"names":[],"mappings":";AAAA,oBAAoB;AACpB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,SAAS,MAAM,iBAAiB,CAAC;AACxC,OAAO,QAAQ,MAAM,WAAW,CAAC;AACjC,OAAO,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.jsx"],"names":[],"mappings":";AAAA,oBAAoB;AACpB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,SAAS,MAAM,iBAAiB,CAAC;AACxC,OAAO,QAAQ,MAAM,WAAW,CAAC;AACjC,OAAO,aAAa,CAAC;AAErB,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,EAAE;IACvB,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IACpF,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,qFAAqF;AACrF,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,EAAE;IAC/B,IAAI,CAAC,KAAK,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,KAAC,mBAAmB,OAAK,KAAK,GAAI,CAAC;AAC5C,CAAC,CAAC;AAEF,qDAAqD;AACrD,0EAA0E;AAC1E,8DAA8D;AAC9D,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,EAAE;IACpC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,GAAG,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IACvH,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrD,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;QACzC,IAAI,EAAE,CAAC;QACP,GAAG,EAAE,CAAC;QACN,MAAM,EAAE,CAAC;QACT,KAAK,EAAE,CAAC;KACT,CAAC,CAAC;IACH,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC/C,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC;QAC9B,MAAM,EAAE,IAAI;KACb,CAAC,CAAC;IACH,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAE7D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,CAAC,wBAAwB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,KAAK,CAAC,eAAe,CAAC,GAAG,EAAE;QACzB,IAAI,SAAS,IAAI,IAAI,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;YAC5C,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC;QACxE,CAAC;aAAM,CAAC;YACN,aAAa,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC;IAEtB,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;QACjC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC;QACtE,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,EAAE,qBAAqB,EAAE,CAAC;QAC/D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,SAAS,CAAC;YACR,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;YACjC,KAAK,EAAE,WAAW,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;YAClD,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC;YAC/B,MAAM,EAAE,YAAY,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;SACtD,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,iBAAiB,GAAG,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE;QACzC,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,CAAC,CAAC,eAAe,EAAE,CAAC;QAEpB,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;QAChE,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;QAC1E,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC;QACzB,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC;QACzB,MAAM,MAAM,GAAG,OAAO,EAAE,WAAW,IAAI,SAAS,CAAC,KAAK,CAAC;QACvD,MAAM,MAAM,GAAG,SAAS,EAAE,YAAY,IAAI,SAAS,CAAC,MAAM,IAAI,GAAG,CAAC;QAClE,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC;QAE7B,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,EAAE;YAChC,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;YACtC,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;YAEtC,YAAY,CAAC,IAAI,CAAC,EAAE;gBAClB,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;gBACtB,IAAI,IAAI,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;gBAEvD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;oBAAE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;gBAC/D,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;oBAAE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;gBAC/D,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;oBAAE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;gBAE/D,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YACvC,CAAC,CAAC,CAAC;YAEH,0DAA0D;YAC1D,2EAA2E;YAC3E,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACvD,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,EAAE,UAAU,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC,CAAC;QAEF,MAAM,SAAS,GAAG,GAAG,EAAE;YACrB,QAAQ,CAAC,mBAAmB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;YACvD,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC,CAAC;QAEF,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QACpD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAClD,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,GAAG,EAAE;QACzB,IAAI,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YAC3C,OAAO;gBACL,IAAI,EAAE;oBACJ,aAAa,EAAE,MAAM;oBACrB,MAAM,EAAE,QAAQ,SAAS,CAAC,MAAM,aAAa;oBAC7C,QAAQ,EAAE,MAAM;oBAChB,OAAO,EAAE,MAAM;oBACf,aAAa,EAAE,QAAQ;oBACvB,SAAS,EAAE,YAAY;iBACxB;gBACD,OAAO,EAAE;oBACP,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,IAAI;oBAC/B,OAAO,EAAE,MAAM;oBACf,aAAa,EAAE,QAAQ;iBACxB;aACF,CAAC;QACJ,CAAC;QAED,OAAO;YACL,IAAI,EAAE;gBACJ,aAAa,EAAE,MAAM;gBACrB,GAAG,CAAC,MAAM,IAAI;oBACZ,MAAM,EAAE,QAAQ,MAAM,WAAW;oBACjC,QAAQ,EAAE,QAAQ;oBAClB,OAAO,EAAE,MAAM;oBACf,aAAa,EAAE,QAAQ;oBACvB,SAAS,EAAE,YAAY;iBACxB,CAAC;aACH;YACD,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;gBAChB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,MAAM;gBACf,aAAa,EAAE,QAAQ;aACxB,CAAC,CAAC,CAAC,EAAE;SACP,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,CACpB,8BACE,cAAK,SAAS,EAAC,yBAAyB,EAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,GAAI,EAC1F,cAAK,SAAS,EAAC,yBAAyB,EAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,GAAI,EAC1F,cAAK,SAAS,EAAC,yBAAyB,EAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,GAAI,EAC1F,cAAK,SAAS,EAAC,0BAA0B,EAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,IAAI,CAAC,GAAI,EAC5F,cAAK,SAAS,EAAC,0BAA0B,EAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,IAAI,CAAC,GAAI,IAC3F,CACJ,CAAC;IAEF,0BAA0B;IAC1B,MAAM,YAAY,GAAG,CACnB,KAAC,KAAK,OACA,KAAK,EACT,IAAI,EAAE,CAAC,aAAa,EACpB,YAAY,EAAE,YAAY,IAAI,KAAK,EACnC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,QAAQ,EAChC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAChD,KAAK,EACH,cACE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EACxC,WAAW,EAAE,GAAG,EAAE;gBAChB,IAAI,QAAQ,EAAE,CAAC;oBACb,WAAW,CAAC,KAAK,CAAC,CAAC;gBACrB,CAAC;YACH,CAAC,EACD,UAAU,EAAE,GAAG,EAAE;gBACf,WAAW,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC,EACD,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,EAClB,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,YAEhB,KAAK,GACF,EAER,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,aAAa,EAAE,EACvB,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CACtB,KAAC,SAAS,IACR,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EACzD,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,YAElD,cAAK,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,YAClD,KAAK,GACF,GACI,CACb,YAED,4BAAG,QAAQ,GAAI,GACT,CACT,CAAC;IAEF,iDAAiD;IACjD,kDAAkD;IAClD,OAAO,CACL,8BACG,OAAO,YAAY,KAAK,UAAU;gBACjC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,YAAY,EAAE,YAAY,EAAE,CAAC;gBACrD,CAAC,CAAC,YAAY,EACf,SAAS,IAAI,UAAU,IAAI,QAAQ,CAAC,YAAY,CAAC,aAAa,EAAE,UAAU,CAAC,IAC3E,CACJ,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
package/dist/style.css
CHANGED
|
@@ -8,3 +8,51 @@
|
|
|
8
8
|
gap: 8px; /* Mezery mezi tlačítky */
|
|
9
9
|
padding-top: 50px;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/* Resize handles */
|
|
13
|
+
.drm-handle {
|
|
14
|
+
position: absolute;
|
|
15
|
+
z-index: 10;
|
|
16
|
+
user-select: none;
|
|
17
|
+
pointer-events: auto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.drm-handle-e {
|
|
21
|
+
top: 12px;
|
|
22
|
+
right: -4px;
|
|
23
|
+
width: 8px;
|
|
24
|
+
height: calc(100% - 24px);
|
|
25
|
+
cursor: e-resize;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.drm-handle-w {
|
|
29
|
+
top: 12px;
|
|
30
|
+
left: -4px;
|
|
31
|
+
width: 8px;
|
|
32
|
+
height: calc(100% - 24px);
|
|
33
|
+
cursor: w-resize;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.drm-handle-s {
|
|
37
|
+
bottom: -4px;
|
|
38
|
+
left: 12px;
|
|
39
|
+
width: calc(100% - 24px);
|
|
40
|
+
height: 8px;
|
|
41
|
+
cursor: s-resize;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.drm-handle-se {
|
|
45
|
+
bottom: -4px;
|
|
46
|
+
right: -4px;
|
|
47
|
+
width: 16px;
|
|
48
|
+
height: 16px;
|
|
49
|
+
cursor: se-resize;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.drm-handle-sw {
|
|
53
|
+
bottom: -4px;
|
|
54
|
+
left: -4px;
|
|
55
|
+
width: 16px;
|
|
56
|
+
height: 16px;
|
|
57
|
+
cursor: sw-resize;
|
|
58
|
+
}
|
package/index.jsx
CHANGED
|
@@ -5,9 +5,23 @@ import Draggable from 'react-draggable';
|
|
|
5
5
|
import ReactDOM from 'react-dom';
|
|
6
6
|
import './style.css';
|
|
7
7
|
|
|
8
|
+
const parseWidth = (w) => {
|
|
9
|
+
if (typeof w === 'number') return w;
|
|
10
|
+
if (typeof w === 'string') { const n = parseInt(w, 10); return isNaN(n) ? 520 : n; }
|
|
11
|
+
return 520;
|
|
12
|
+
};
|
|
8
13
|
|
|
14
|
+
// Wrapper – podmíněné renderování = destroy on close (kompletní unmount při zavření)
|
|
9
15
|
const DraggableModal = (props) => {
|
|
10
|
-
|
|
16
|
+
if (!props.open) return null;
|
|
17
|
+
return <DraggableModalInner {...props} />;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Inner komponenta – obsahuje veškerou logiku a stav
|
|
21
|
+
// Při zavření se kompletně unmountuje → všechny hooks a stav jsou zničeny
|
|
22
|
+
// Při otevření se vytvoří čerstvá instance → žádný starý stav
|
|
23
|
+
const DraggableModalInner = (props) => {
|
|
24
|
+
const { open, onOk, onCancel, isInteractive = false, title, children, getContainer, height, resizable = true } = props;
|
|
11
25
|
const draggleRef = React.useRef(null);
|
|
12
26
|
const [disabled, setDisabled] = React.useState(true);
|
|
13
27
|
const [bounds, setBounds] = React.useState({
|
|
@@ -16,6 +30,12 @@ const DraggableModal = (props) => {
|
|
|
16
30
|
bottom: 0,
|
|
17
31
|
right: 0,
|
|
18
32
|
});
|
|
33
|
+
const [modalSize, setModalSize] = React.useState({
|
|
34
|
+
width: parseWidth(props.width),
|
|
35
|
+
height: null,
|
|
36
|
+
});
|
|
37
|
+
const [antModalEl, setAntModalEl] = React.useState(null);
|
|
38
|
+
const [dragPos, setDragPos] = React.useState({ x: 0, y: 0 });
|
|
19
39
|
|
|
20
40
|
React.useEffect(() => {
|
|
21
41
|
if (isInteractive) {
|
|
@@ -23,6 +43,14 @@ const DraggableModal = (props) => {
|
|
|
23
43
|
}
|
|
24
44
|
}, [isInteractive]);
|
|
25
45
|
|
|
46
|
+
React.useLayoutEffect(() => {
|
|
47
|
+
if (resizable && open && draggleRef.current) {
|
|
48
|
+
setAntModalEl(draggleRef.current.querySelector('.ant-modal-content'));
|
|
49
|
+
} else {
|
|
50
|
+
setAntModalEl(null);
|
|
51
|
+
}
|
|
52
|
+
}, [resizable, open]);
|
|
53
|
+
|
|
26
54
|
const onStart = (_event, uiData) => {
|
|
27
55
|
const { clientWidth, clientHeight } = window.document.documentElement;
|
|
28
56
|
const targetRect = draggleRef.current?.getBoundingClientRect();
|
|
@@ -37,6 +65,97 @@ const DraggableModal = (props) => {
|
|
|
37
65
|
});
|
|
38
66
|
};
|
|
39
67
|
|
|
68
|
+
const handleResizeStart = (e, direction) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
|
|
72
|
+
const modalEl = draggleRef.current?.querySelector('.ant-modal');
|
|
73
|
+
const contentEl = draggleRef.current?.querySelector('.ant-modal-content');
|
|
74
|
+
const startX = e.clientX;
|
|
75
|
+
const startY = e.clientY;
|
|
76
|
+
const startW = modalEl?.offsetWidth || modalSize.width;
|
|
77
|
+
const startH = contentEl?.offsetHeight || modalSize.height || 400;
|
|
78
|
+
const startDragX = dragPos.x;
|
|
79
|
+
|
|
80
|
+
const onMouseMove = (moveEvent) => {
|
|
81
|
+
const dx = moveEvent.clientX - startX;
|
|
82
|
+
const dy = moveEvent.clientY - startY;
|
|
83
|
+
|
|
84
|
+
setModalSize(prev => {
|
|
85
|
+
let newW = prev.width;
|
|
86
|
+
let newH = prev.height !== null ? prev.height : startH;
|
|
87
|
+
|
|
88
|
+
if (direction.includes('e')) newW = Math.max(300, startW + dx);
|
|
89
|
+
if (direction.includes('w')) newW = Math.max(300, startW - dx);
|
|
90
|
+
if (direction.includes('s')) newH = Math.max(200, startH + dy);
|
|
91
|
+
|
|
92
|
+
return { width: newW, height: newH };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Kompenzace: Ant Design centruje modal přes margin:auto.
|
|
96
|
+
// Při změně width se levý okraj posune o dx/2 — opravíme pozici Draggable.
|
|
97
|
+
if (direction.includes('e') || direction.includes('w')) {
|
|
98
|
+
setDragPos(prev => ({ ...prev, x: startDragX + dx / 2 }));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onMouseUp = () => {
|
|
103
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
104
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
108
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const resolveStyles = () => {
|
|
112
|
+
if (resizable && modalSize.height !== null) {
|
|
113
|
+
return {
|
|
114
|
+
body: {
|
|
115
|
+
paddingBottom: '30px',
|
|
116
|
+
height: `calc(${modalSize.height}px - 110px)`,
|
|
117
|
+
overflow: 'auto',
|
|
118
|
+
display: 'flex',
|
|
119
|
+
flexDirection: 'column',
|
|
120
|
+
boxSizing: 'border-box',
|
|
121
|
+
},
|
|
122
|
+
content: {
|
|
123
|
+
height: `${modalSize.height}px`,
|
|
124
|
+
display: 'flex',
|
|
125
|
+
flexDirection: 'column',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
body: {
|
|
132
|
+
paddingBottom: '30px',
|
|
133
|
+
...(height && {
|
|
134
|
+
height: `calc(${height} - 110px)`,
|
|
135
|
+
overflow: 'hidden',
|
|
136
|
+
display: 'flex',
|
|
137
|
+
flexDirection: 'column',
|
|
138
|
+
boxSizing: 'border-box',
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
content: height ? {
|
|
142
|
+
height: height,
|
|
143
|
+
display: 'flex',
|
|
144
|
+
flexDirection: 'column',
|
|
145
|
+
} : {},
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const resizeHandles = (
|
|
150
|
+
<>
|
|
151
|
+
<div className="drm-handle drm-handle-e" onMouseDown={(e) => handleResizeStart(e, 'e')} />
|
|
152
|
+
<div className="drm-handle drm-handle-w" onMouseDown={(e) => handleResizeStart(e, 'w')} />
|
|
153
|
+
<div className="drm-handle drm-handle-s" onMouseDown={(e) => handleResizeStart(e, 's')} />
|
|
154
|
+
<div className="drm-handle drm-handle-se" onMouseDown={(e) => handleResizeStart(e, 'se')} />
|
|
155
|
+
<div className="drm-handle drm-handle-sw" onMouseDown={(e) => handleResizeStart(e, 'sw')} />
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
|
|
40
159
|
// Sdílený Modal komponent
|
|
41
160
|
const modalContent = (
|
|
42
161
|
<Modal
|
|
@@ -44,7 +163,7 @@ const DraggableModal = (props) => {
|
|
|
44
163
|
mask={!isInteractive}
|
|
45
164
|
getContainer={getContainer || false} // false = renderovat v místě
|
|
46
165
|
zIndex={props.zIndex || 99999999}
|
|
47
|
-
|
|
166
|
+
width={resizable ? modalSize.width : props.width}
|
|
48
167
|
title={
|
|
49
168
|
<div
|
|
50
169
|
style={{ width: '100%', cursor: 'move' }}
|
|
@@ -65,29 +184,18 @@ const DraggableModal = (props) => {
|
|
|
65
184
|
open={open}
|
|
66
185
|
onOk={onOk}
|
|
67
186
|
onCancel={onCancel}
|
|
68
|
-
styles={
|
|
69
|
-
body: {
|
|
70
|
-
paddingBottom: '30px',
|
|
71
|
-
...(height && {
|
|
72
|
-
height: `calc(${height} - 110px)`,
|
|
73
|
-
overflow: 'hidden',
|
|
74
|
-
display: 'flex',
|
|
75
|
-
flexDirection: 'column'
|
|
76
|
-
})
|
|
77
|
-
},
|
|
78
|
-
content: height ? {
|
|
79
|
-
height: height,
|
|
80
|
-
display: 'flex',
|
|
81
|
-
flexDirection: 'column'
|
|
82
|
-
} : {}
|
|
83
|
-
}}
|
|
187
|
+
styles={resolveStyles()}
|
|
84
188
|
modalRender={(modal) => (
|
|
85
189
|
<Draggable
|
|
86
190
|
disabled={disabled}
|
|
87
191
|
bounds={bounds}
|
|
192
|
+
position={dragPos}
|
|
193
|
+
onDrag={(e, data) => setDragPos({ x: data.x, y: data.y })}
|
|
88
194
|
onStart={(event, uiData) => onStart(event, uiData)}
|
|
89
195
|
>
|
|
90
|
-
<div ref={draggleRef}
|
|
196
|
+
<div ref={draggleRef} style={{ position: 'relative' }}>
|
|
197
|
+
{modal}
|
|
198
|
+
</div>
|
|
91
199
|
</Draggable>
|
|
92
200
|
)}
|
|
93
201
|
>
|
|
@@ -97,13 +205,14 @@ const DraggableModal = (props) => {
|
|
|
97
205
|
|
|
98
206
|
// Pokud je getContainer funkce, použijeme portal
|
|
99
207
|
// Jinak renderujeme přímo (zachová React context)
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
{typeof getContainer === 'function'
|
|
211
|
+
? ReactDOM.createPortal(modalContent, getContainer())
|
|
212
|
+
: modalContent}
|
|
213
|
+
{resizable && antModalEl && ReactDOM.createPortal(resizeHandles, antModalEl)}
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
107
216
|
};
|
|
108
217
|
|
|
109
|
-
export default DraggableModal;
|
|
218
|
+
export default DraggableModal;
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bit.rhplus/draggable-modal",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"homepage": "https://bit.cloud/remote-scope/draggable-modal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"componentId": {
|
|
7
7
|
"scope": "remote-scope",
|
|
8
8
|
"name": "draggable-modal",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.15"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"antd": "^5.20.6",
|
|
13
13
|
"react-draggable": "^4.4.6"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@teambit/react.react-env": "1.
|
|
16
|
+
"@teambit/react.react-env": "1.3.1"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"react": "^17.0.0 || ^18.0.0",
|
|
20
|
-
"react-dom": "^17.0.0 || ^18.0.0"
|
|
19
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
20
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
21
21
|
},
|
|
22
22
|
"license": "SEE LICENSE IN UNLICENSED",
|
|
23
23
|
"optionalDependencies": {},
|
package/style.css
CHANGED
|
@@ -8,3 +8,51 @@
|
|
|
8
8
|
gap: 8px; /* Mezery mezi tlačítky */
|
|
9
9
|
padding-top: 50px;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/* Resize handles */
|
|
13
|
+
.drm-handle {
|
|
14
|
+
position: absolute;
|
|
15
|
+
z-index: 10;
|
|
16
|
+
user-select: none;
|
|
17
|
+
pointer-events: auto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.drm-handle-e {
|
|
21
|
+
top: 12px;
|
|
22
|
+
right: -4px;
|
|
23
|
+
width: 8px;
|
|
24
|
+
height: calc(100% - 24px);
|
|
25
|
+
cursor: e-resize;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.drm-handle-w {
|
|
29
|
+
top: 12px;
|
|
30
|
+
left: -4px;
|
|
31
|
+
width: 8px;
|
|
32
|
+
height: calc(100% - 24px);
|
|
33
|
+
cursor: w-resize;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.drm-handle-s {
|
|
37
|
+
bottom: -4px;
|
|
38
|
+
left: 12px;
|
|
39
|
+
width: calc(100% - 24px);
|
|
40
|
+
height: 8px;
|
|
41
|
+
cursor: s-resize;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.drm-handle-se {
|
|
45
|
+
bottom: -4px;
|
|
46
|
+
right: -4px;
|
|
47
|
+
width: 16px;
|
|
48
|
+
height: 16px;
|
|
49
|
+
cursor: se-resize;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.drm-handle-sw {
|
|
53
|
+
bottom: -4px;
|
|
54
|
+
left: -4px;
|
|
55
|
+
width: 16px;
|
|
56
|
+
height: 16px;
|
|
57
|
+
cursor: sw-resize;
|
|
58
|
+
}
|
|
File without changes
|