@huaqiu/component-gen-app 0.4.7 → 0.4.9
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/assets/index-BnNwBC2r.js +553 -0
- package/dist/index.html +1 -1
- package/lib/index.js +109 -1
- package/package.json +1 -1
- package/src/components/UploadInput.tsx +54 -3
- package/src/utils/drag.ts +72 -0
- package/dist/assets/index-t51t_a62.js +0 -553
package/dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>华秋 Component Gen</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BnNwBC2r.js"></script>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
package/lib/index.js
CHANGED
|
@@ -337,6 +337,81 @@ function translateFor(lang) {
|
|
|
337
337
|
/** zh-only fallback (used when no locale is supplied). */
|
|
338
338
|
const defaultT = (key, params) => translate("zh", key, params);
|
|
339
339
|
//#endregion
|
|
340
|
+
//#region src/utils/drag.ts
|
|
341
|
+
/**
|
|
342
|
+
* `@huaqiu/component-gen-app` — page-level drag-state interop.
|
|
343
|
+
*
|
|
344
|
+
* The app is rendered inside hosts that own a *page-wide* file drop target
|
|
345
|
+
* (dsh's attachment plugin listens on `document` for dragenter/dragover/
|
|
346
|
+
* dragleave/drop and paints a full-screen "drop to attach" invitation). Two
|
|
347
|
+
* properties of that pattern matter to any nested drop zone:
|
|
348
|
+
*
|
|
349
|
+
* - `dragover` writes `dataTransfer.dropEffect = 'none'` whenever the host
|
|
350
|
+
* refuses the drop (no session, composer busy, …). `dropEffect` lives on the
|
|
351
|
+
* drag data store shared by the whole drag, so the host's verdict outranks
|
|
352
|
+
* the nested zone's: Chromium then cancels the drag operation and the `drop`
|
|
353
|
+
* event is never dispatched anywhere on the page. The nested zone is dead
|
|
354
|
+
* even though its own `dragover` called `preventDefault()`.
|
|
355
|
+
* - The host counts dragenter/dragleave to drive that invitation, and only
|
|
356
|
+
* clears the counter from its own `drop`, `dragleave`, or window `dragend`.
|
|
357
|
+
* A drag of OS files fires **no** `dragend` ("dragstart and dragend are not
|
|
358
|
+
* fired when dragging a file into the browser from the OS" — MDN, File drag
|
|
359
|
+
* and drop), so a nested zone that swallows the drop leaves the host's
|
|
360
|
+
* invitation dimming the screen forever.
|
|
361
|
+
*
|
|
362
|
+
* A nested zone therefore has to do two things: keep its own drag events to
|
|
363
|
+
* itself (`stopPropagation`, done by the drop zone) and tell the page when the
|
|
364
|
+
* drag has moved onto it / been consumed, which is what this module is for.
|
|
365
|
+
*/
|
|
366
|
+
/**
|
|
367
|
+
* Broadcast "this file drag is no longer yours" to page-wide drop targets.
|
|
368
|
+
*
|
|
369
|
+
* Dispatched on `document.body` with a Files-carrying `dataTransfer`, which is
|
|
370
|
+
* the shape page-wide drop targets test for, and at the viewport origin, which
|
|
371
|
+
* is how they recognise "the pointer left the page" and drop their drag state.
|
|
372
|
+
* Holding a page-wide target's enter/leave counter at zero while the pointer is
|
|
373
|
+
* over our own drop zone is also what lets the zone be seen: dsh's invitation
|
|
374
|
+
* paints a blurred full-viewport mask, and it is the host's own handler that
|
|
375
|
+
* has to take it back down.
|
|
376
|
+
*
|
|
377
|
+
* Idempotent and a no-op in hosts without a page-wide drop target (the
|
|
378
|
+
* standalone build, SSR, environments without `DataTransfer`).
|
|
379
|
+
*/
|
|
380
|
+
function releasePageDragState() {
|
|
381
|
+
if (typeof document === "undefined" || !document.body) return;
|
|
382
|
+
const event = new Event("dragleave", {
|
|
383
|
+
bubbles: true,
|
|
384
|
+
cancelable: true
|
|
385
|
+
});
|
|
386
|
+
Object.assign(event, {
|
|
387
|
+
clientX: 0,
|
|
388
|
+
clientY: 0,
|
|
389
|
+
dataTransfer: dragStateTransfer()
|
|
390
|
+
});
|
|
391
|
+
document.body.dispatchEvent(event);
|
|
392
|
+
}
|
|
393
|
+
/** Minimal Files payload page-wide drop targets recognise. */
|
|
394
|
+
function dragStateTransfer() {
|
|
395
|
+
try {
|
|
396
|
+
const transfer = new DataTransfer();
|
|
397
|
+
transfer.items.add(new File([], "drag-state"));
|
|
398
|
+
return transfer;
|
|
399
|
+
} catch {
|
|
400
|
+
return { types: ["Files"] };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* True when a drag carries files (as opposed to text or an in-page element
|
|
405
|
+
* being dragged). Guards the drag-state broadcast so text drags over the
|
|
406
|
+
* upload zone do not disturb hosts tracking their own drags.
|
|
407
|
+
*/
|
|
408
|
+
function dragHasFiles(dataTransfer) {
|
|
409
|
+
if (!dataTransfer) return false;
|
|
410
|
+
const types = dataTransfer.types;
|
|
411
|
+
if (!types) return false;
|
|
412
|
+
return Array.from(types).some((type) => type === "Files" || type === "application/x-moz-file");
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
340
415
|
//#region src/components/UploadInput.tsx
|
|
341
416
|
/**
|
|
342
417
|
* `@huaqiu/component-gen-app` — image upload control (click + paste + drag).
|
|
@@ -344,6 +419,13 @@ const defaultT = (key, params) => translate("zh", key, params);
|
|
|
344
419
|
* Reads a file, downscales it to a thumbnail data URL if needed (bounded by
|
|
345
420
|
* `maxBytes`), and reports both the original file and the data URL to the
|
|
346
421
|
* parent. The parent owns sending it to the server.
|
|
422
|
+
*
|
|
423
|
+
* Drag-and-drop note: the zone has to *claim* the drag. Hosts that install a
|
|
424
|
+
* page-wide file drop target (dsh's attachment plugin does) otherwise either
|
|
425
|
+
* cancel the drag outright — their `dragover` writes `dropEffect = 'none'` into
|
|
426
|
+
* the shared drag data store, which stops `drop` from ever being dispatched —
|
|
427
|
+
* or take the dropped file for themselves at document level. See
|
|
428
|
+
* `utils/drag.ts` for the full mechanism.
|
|
347
429
|
*/
|
|
348
430
|
const MAX_EDGE = 1600;
|
|
349
431
|
/** Downscale an image file to a data URL bounded in edge + bytes. */
|
|
@@ -391,6 +473,7 @@ function UploadInput(props) {
|
|
|
391
473
|
const inputRef = useRef(null);
|
|
392
474
|
const [dragging, setDragging] = useState(false);
|
|
393
475
|
const [error, setError] = useState(null);
|
|
476
|
+
const dragDepth = useRef(0);
|
|
394
477
|
const accept = useCallback(async (f) => {
|
|
395
478
|
if (!f) return;
|
|
396
479
|
if (f.size > maxBytes) {
|
|
@@ -409,19 +492,44 @@ function UploadInput(props) {
|
|
|
409
492
|
onFile,
|
|
410
493
|
t
|
|
411
494
|
]);
|
|
495
|
+
/** Keep a drag event away from page-wide drop targets (see the module docs). */
|
|
496
|
+
const claim = (ev) => {
|
|
497
|
+
ev.stopPropagation();
|
|
498
|
+
};
|
|
499
|
+
/** The drag now belongs to the page again — let its drop targets resume. */
|
|
500
|
+
const release = (ev) => {
|
|
501
|
+
if (dragHasFiles(ev.dataTransfer)) releasePageDragState();
|
|
502
|
+
};
|
|
412
503
|
return /* @__PURE__ */ jsxs("div", {
|
|
413
504
|
className: `cga-upload${dragging ? " cga-upload--dragging" : ""}`,
|
|
414
505
|
onClick: () => {
|
|
415
506
|
if (!disabled) inputRef.current?.click();
|
|
416
507
|
},
|
|
508
|
+
onDragEnter: (ev) => {
|
|
509
|
+
ev.preventDefault();
|
|
510
|
+
claim(ev);
|
|
511
|
+
dragDepth.current += 1;
|
|
512
|
+
if (dragDepth.current === 1) {
|
|
513
|
+
setDragging(true);
|
|
514
|
+
release(ev);
|
|
515
|
+
}
|
|
516
|
+
},
|
|
417
517
|
onDragOver: (ev) => {
|
|
418
518
|
ev.preventDefault();
|
|
519
|
+
claim(ev);
|
|
520
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = disabled ? "none" : "copy";
|
|
419
521
|
if (!disabled) setDragging(true);
|
|
420
522
|
},
|
|
421
|
-
onDragLeave: () =>
|
|
523
|
+
onDragLeave: (ev) => {
|
|
524
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
525
|
+
if (dragDepth.current === 0) setDragging(false);
|
|
526
|
+
},
|
|
422
527
|
onDrop: (ev) => {
|
|
423
528
|
ev.preventDefault();
|
|
529
|
+
claim(ev);
|
|
530
|
+
dragDepth.current = 0;
|
|
424
531
|
setDragging(false);
|
|
532
|
+
release(ev);
|
|
425
533
|
if (disabled) return;
|
|
426
534
|
accept(ev.dataTransfer.files?.[0] ?? null);
|
|
427
535
|
},
|
package/package.json
CHANGED
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
* Reads a file, downscales it to a thumbnail data URL if needed (bounded by
|
|
5
5
|
* `maxBytes`), and reports both the original file and the data URL to the
|
|
6
6
|
* parent. The parent owns sending it to the server.
|
|
7
|
+
*
|
|
8
|
+
* Drag-and-drop note: the zone has to *claim* the drag. Hosts that install a
|
|
9
|
+
* page-wide file drop target (dsh's attachment plugin does) otherwise either
|
|
10
|
+
* cancel the drag outright — their `dragover` writes `dropEffect = 'none'` into
|
|
11
|
+
* the shared drag data store, which stops `drop` from ever being dispatched —
|
|
12
|
+
* or take the dropped file for themselves at document level. See
|
|
13
|
+
* `utils/drag.ts` for the full mechanism.
|
|
7
14
|
*/
|
|
8
|
-
import { useCallback, useRef, useState, type ReactElement } from 'react'
|
|
15
|
+
import { useCallback, useRef, useState, type DragEvent as ReactDragEvent, type ReactElement } from 'react'
|
|
9
16
|
import type { Translate } from '../copy/index.js'
|
|
17
|
+
import { dragHasFiles, releasePageDragState } from '../utils/drag.js'
|
|
10
18
|
|
|
11
19
|
export interface UploadInputProps {
|
|
12
20
|
maxBytes: number
|
|
@@ -58,6 +66,10 @@ export function UploadInput(props: UploadInputProps): ReactElement {
|
|
|
58
66
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
59
67
|
const [dragging, setDragging] = useState(false)
|
|
60
68
|
const [error, setError] = useState<string | null>(null)
|
|
69
|
+
// Enter/leave counter instead of a boolean: the browser fires dragenter and
|
|
70
|
+
// dragleave for *every* element transition inside the zone, so a boolean
|
|
71
|
+
// flickers off whenever the pointer crosses the label, thumbnail or button.
|
|
72
|
+
const dragDepth = useRef(0)
|
|
61
73
|
|
|
62
74
|
const accept = useCallback(async (f: File | null): Promise<void> => {
|
|
63
75
|
if (!f) return
|
|
@@ -74,15 +86,54 @@ export function UploadInput(props: UploadInputProps): ReactElement {
|
|
|
74
86
|
}
|
|
75
87
|
}, [maxBytes, onFile, t])
|
|
76
88
|
|
|
89
|
+
/** Keep a drag event away from page-wide drop targets (see the module docs). */
|
|
90
|
+
const claim = (ev: ReactDragEvent<HTMLDivElement>): void => {
|
|
91
|
+
ev.stopPropagation()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The drag now belongs to the page again — let its drop targets resume. */
|
|
95
|
+
const release = (ev: ReactDragEvent<HTMLDivElement>): void => {
|
|
96
|
+
if (dragHasFiles(ev.dataTransfer)) releasePageDragState()
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
return (
|
|
78
100
|
<div
|
|
79
101
|
className={`cga-upload${dragging ? ' cga-upload--dragging' : ''}`}
|
|
80
102
|
onClick={() => { if (!disabled) inputRef.current?.click() }}
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
onDragEnter={(ev) => {
|
|
104
|
+
ev.preventDefault()
|
|
105
|
+
claim(ev)
|
|
106
|
+
dragDepth.current += 1
|
|
107
|
+
if (dragDepth.current === 1) {
|
|
108
|
+
setDragging(true)
|
|
109
|
+
// Take the page's drag state down with us: while the file is over the
|
|
110
|
+
// zone the host's full-viewport drop invitation must not cover it.
|
|
111
|
+
release(ev)
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
onDragOver={(ev) => {
|
|
115
|
+
ev.preventDefault()
|
|
116
|
+
claim(ev)
|
|
117
|
+
// Our verdict, not the host's: a 'none' written by a document-level
|
|
118
|
+
// handler reaches the drag data store too, and the browser cancels the
|
|
119
|
+
// whole drag when the last word on it was 'none'.
|
|
120
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = disabled ? 'none' : 'copy'
|
|
121
|
+
if (!disabled) setDragging(true)
|
|
122
|
+
}}
|
|
123
|
+
onDragLeave={(ev) => {
|
|
124
|
+
// Not claimed: the host's own enter/leave accounting stays balanced
|
|
125
|
+
// when the file is dragged back out of the zone.
|
|
126
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1)
|
|
127
|
+
if (dragDepth.current === 0) setDragging(false)
|
|
128
|
+
}}
|
|
83
129
|
onDrop={(ev) => {
|
|
84
130
|
ev.preventDefault()
|
|
131
|
+
// Claimed for good: the host must not also attach this file to its own
|
|
132
|
+
// draft (the point of the drop is the generator, not the composer).
|
|
133
|
+
claim(ev)
|
|
134
|
+
dragDepth.current = 0
|
|
85
135
|
setDragging(false)
|
|
136
|
+
release(ev)
|
|
86
137
|
if (disabled) return
|
|
87
138
|
void accept(ev.dataTransfer.files?.[0] ?? null)
|
|
88
139
|
}}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-app` — page-level drag-state interop.
|
|
3
|
+
*
|
|
4
|
+
* The app is rendered inside hosts that own a *page-wide* file drop target
|
|
5
|
+
* (dsh's attachment plugin listens on `document` for dragenter/dragover/
|
|
6
|
+
* dragleave/drop and paints a full-screen "drop to attach" invitation). Two
|
|
7
|
+
* properties of that pattern matter to any nested drop zone:
|
|
8
|
+
*
|
|
9
|
+
* - `dragover` writes `dataTransfer.dropEffect = 'none'` whenever the host
|
|
10
|
+
* refuses the drop (no session, composer busy, …). `dropEffect` lives on the
|
|
11
|
+
* drag data store shared by the whole drag, so the host's verdict outranks
|
|
12
|
+
* the nested zone's: Chromium then cancels the drag operation and the `drop`
|
|
13
|
+
* event is never dispatched anywhere on the page. The nested zone is dead
|
|
14
|
+
* even though its own `dragover` called `preventDefault()`.
|
|
15
|
+
* - The host counts dragenter/dragleave to drive that invitation, and only
|
|
16
|
+
* clears the counter from its own `drop`, `dragleave`, or window `dragend`.
|
|
17
|
+
* A drag of OS files fires **no** `dragend` ("dragstart and dragend are not
|
|
18
|
+
* fired when dragging a file into the browser from the OS" — MDN, File drag
|
|
19
|
+
* and drop), so a nested zone that swallows the drop leaves the host's
|
|
20
|
+
* invitation dimming the screen forever.
|
|
21
|
+
*
|
|
22
|
+
* A nested zone therefore has to do two things: keep its own drag events to
|
|
23
|
+
* itself (`stopPropagation`, done by the drop zone) and tell the page when the
|
|
24
|
+
* drag has moved onto it / been consumed, which is what this module is for.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Broadcast "this file drag is no longer yours" to page-wide drop targets.
|
|
29
|
+
*
|
|
30
|
+
* Dispatched on `document.body` with a Files-carrying `dataTransfer`, which is
|
|
31
|
+
* the shape page-wide drop targets test for, and at the viewport origin, which
|
|
32
|
+
* is how they recognise "the pointer left the page" and drop their drag state.
|
|
33
|
+
* Holding a page-wide target's enter/leave counter at zero while the pointer is
|
|
34
|
+
* over our own drop zone is also what lets the zone be seen: dsh's invitation
|
|
35
|
+
* paints a blurred full-viewport mask, and it is the host's own handler that
|
|
36
|
+
* has to take it back down.
|
|
37
|
+
*
|
|
38
|
+
* Idempotent and a no-op in hosts without a page-wide drop target (the
|
|
39
|
+
* standalone build, SSR, environments without `DataTransfer`).
|
|
40
|
+
*/
|
|
41
|
+
export function releasePageDragState(): void {
|
|
42
|
+
if (typeof document === 'undefined' || !document.body) return
|
|
43
|
+
// A plain `Event` carrying the same shape, so this works in every engine
|
|
44
|
+
// (notably jsdom, which implements neither `DragEvent` nor `DataTransfer`).
|
|
45
|
+
const event = new Event('dragleave', { bubbles: true, cancelable: true })
|
|
46
|
+
Object.assign(event, { clientX: 0, clientY: 0, dataTransfer: dragStateTransfer() })
|
|
47
|
+
document.body.dispatchEvent(event)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Minimal Files payload page-wide drop targets recognise. */
|
|
51
|
+
function dragStateTransfer(): unknown {
|
|
52
|
+
try {
|
|
53
|
+
const transfer = new DataTransfer()
|
|
54
|
+
// The `types` list only reports `Files` once a file is actually held.
|
|
55
|
+
transfer.items.add(new File([], 'drag-state'))
|
|
56
|
+
return transfer
|
|
57
|
+
} catch {
|
|
58
|
+
return { types: ['Files'] }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* True when a drag carries files (as opposed to text or an in-page element
|
|
64
|
+
* being dragged). Guards the drag-state broadcast so text drags over the
|
|
65
|
+
* upload zone do not disturb hosts tracking their own drags.
|
|
66
|
+
*/
|
|
67
|
+
export function dragHasFiles(dataTransfer: DataTransfer | null | undefined): boolean {
|
|
68
|
+
if (!dataTransfer) return false
|
|
69
|
+
const types = dataTransfer.types
|
|
70
|
+
if (!types) return false
|
|
71
|
+
return Array.from(types).some((type) => type === 'Files' || type === 'application/x-moz-file')
|
|
72
|
+
}
|