@fluxfiles/react 1.0.0

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.
@@ -0,0 +1,138 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import type { FluxFile, FluxEvent, FluxFilesConfig, FluxFilesHandle, FluxMessage } from './types';
3
+
4
+ const SOURCE = 'fluxfiles';
5
+ const VERSION = 1;
6
+
7
+ function uid(): string {
8
+ return 'ff-' + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
9
+ }
10
+
11
+ interface UseFluxFilesOptions extends FluxFilesConfig {
12
+ onSelect?: (file: FluxFile) => void;
13
+ onClose?: () => void;
14
+ onReady?: () => void;
15
+ onEvent?: (event: FluxEvent) => void;
16
+ }
17
+
18
+ /**
19
+ * Low-level hook that manages the postMessage bridge to a FluxFiles iframe.
20
+ *
21
+ * Returns a ref callback to attach to the iframe element, plus command helpers.
22
+ */
23
+ export function useFluxFiles(options: UseFluxFilesOptions): FluxFilesHandle & {
24
+ /** Ref callback — attach to the <iframe> element. */
25
+ iframeRef: (el: HTMLIFrameElement | null) => void;
26
+ /** The iframe src URL. */
27
+ iframeSrc: string;
28
+ } {
29
+ const iframeElRef = useRef<HTMLIFrameElement | null>(null);
30
+ const [ready, setReady] = useState(false);
31
+ const optionsRef = useRef(options);
32
+ optionsRef.current = options;
33
+
34
+ const endpoint = (options.endpoint || '').replace(/\/+$/, '');
35
+ const iframeSrc = endpoint + '/public/index.html';
36
+
37
+ // Post a message to the iframe
38
+ const post = useCallback((type: string, payload: Record<string, unknown> = {}) => {
39
+ const el = iframeElRef.current;
40
+ if (!el?.contentWindow) return;
41
+ el.contentWindow.postMessage(
42
+ { source: SOURCE, type, v: VERSION, id: uid(), payload },
43
+ '*'
44
+ );
45
+ }, []);
46
+
47
+ // Send config when ready
48
+ const sendConfig = useCallback(() => {
49
+ const opts = optionsRef.current;
50
+ post('FM_CONFIG', {
51
+ disk: opts.disk || 'local',
52
+ token: opts.token || '',
53
+ mode: opts.mode || 'picker',
54
+ allowedTypes: opts.allowedTypes || null,
55
+ maxSize: opts.maxSize || null,
56
+ endpoint: opts.endpoint || '',
57
+ locale: opts.locale || null,
58
+ });
59
+ }, [post]);
60
+
61
+ // Listen for messages from iframe
62
+ useEffect(() => {
63
+ function onMessage(e: MessageEvent) {
64
+ const msg = e.data as FluxMessage;
65
+ if (!msg || msg.source !== SOURCE) return;
66
+
67
+ const opts = optionsRef.current;
68
+
69
+ switch (msg.type) {
70
+ case 'FM_READY':
71
+ setReady(true);
72
+ sendConfig();
73
+ opts.onReady?.();
74
+ break;
75
+ case 'FM_SELECT':
76
+ opts.onSelect?.(msg.payload as unknown as FluxFile);
77
+ break;
78
+ case 'FM_EVENT':
79
+ opts.onEvent?.(msg.payload as unknown as FluxEvent);
80
+ break;
81
+ case 'FM_CLOSE':
82
+ opts.onClose?.();
83
+ break;
84
+ }
85
+ }
86
+
87
+ window.addEventListener('message', onMessage);
88
+ return () => {
89
+ window.removeEventListener('message', onMessage);
90
+ };
91
+ }, [sendConfig]);
92
+
93
+ // Re-send config when token or disk changes
94
+ useEffect(() => {
95
+ if (ready) {
96
+ sendConfig();
97
+ }
98
+ }, [options.token, options.disk, options.mode, options.locale, ready, sendConfig]);
99
+
100
+ // Command helpers
101
+ const command = useCallback(
102
+ (action: string, data: Record<string, unknown> = {}) => {
103
+ post('FM_COMMAND', { action, ...data });
104
+ },
105
+ [post]
106
+ );
107
+
108
+ const navigate = useCallback((path: string) => command('navigate', { path }), [command]);
109
+ const setDisk = useCallback((disk: string) => command('setDisk', { disk }), [command]);
110
+ const refresh = useCallback(() => command('refresh'), [command]);
111
+ const search = useCallback((q: string) => command('search', { q }), [command]);
112
+ const crossCopy = useCallback((dstDisk: string, dstPath?: string) => command('crossCopy', { dst_disk: dstDisk, dst_path: dstPath || '' }), [command]);
113
+ const crossMove = useCallback((dstDisk: string, dstPath?: string) => command('crossMove', { dst_disk: dstDisk, dst_path: dstPath || '' }), [command]);
114
+ const crop = useCallback((x: number, y: number, width: number, height: number, savePath?: string) => command('crop', { x, y, width, height, save_path: savePath || '' }), [command]);
115
+ const aiTag = useCallback(() => command('aiTag'), [command]);
116
+
117
+ const iframeRef = useCallback((el: HTMLIFrameElement | null) => {
118
+ iframeElRef.current = el;
119
+ if (!el) {
120
+ setReady(false);
121
+ }
122
+ }, []);
123
+
124
+ return {
125
+ iframeRef,
126
+ iframeSrc,
127
+ ready,
128
+ command,
129
+ navigate,
130
+ setDisk,
131
+ refresh,
132
+ search,
133
+ crossCopy,
134
+ crossMove,
135
+ crop,
136
+ aiTag,
137
+ };
138
+ }