tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
handle dropping images
awarm.space
4 months ago
c1ad6813
dfc1d599
+150
-26
4 changed files
expand all
collapse all
unified
split
components
Blocks
Block.tsx
ImageBlock.tsx
index.tsx
useHandleDrop.ts
+19
-1
components/Blocks/Block.tsx
···
7
7
import { useBlockKeyboardHandlers } from "./useBlockKeyboardHandlers";
8
8
import { useLongPress } from "src/hooks/useLongPress";
9
9
import { focusBlock } from "src/utils/focusBlock";
10
10
+
import { useHandleDrop } from "./useHandleDrop";
11
11
+
import { useEntitySetContext } from "components/EntitySetProvider";
10
12
11
13
import { TextBlock } from "components/Blocks/TextBlock";
12
14
import { ImageBlock } from "./ImageBlock";
···
15
17
import { EmbedBlock } from "./EmbedBlock";
16
18
import { MailboxBlock } from "./MailboxBlock";
17
19
import { AreYouSure } from "./DeleteBlock";
18
18
-
import { useEntitySetContext } from "components/EntitySetProvider";
19
20
import { useIsMobile } from "src/hooks/isMobile";
20
21
import { DateTimeBlock } from "./DateTimeBlock";
21
22
import { RSVPBlock } from "./RSVPBlock";
···
63
64
// and shared styling like padding and flex for list layouting
64
65
65
66
let mouseHandlers = useBlockMouseHandlers(props);
67
67
+
let handleDrop = useHandleDrop({
68
68
+
parent: props.parent,
69
69
+
position: props.position,
70
70
+
nextPosition: props.nextPosition,
71
71
+
});
72
72
+
let entity_set = useEntitySetContext();
66
73
67
74
let { isLongPress, handlers } = useLongPress(() => {
68
75
if (isTextBlock[props.type]) return;
···
93
100
{...(!props.preview ? { ...mouseHandlers, ...handlers } : {})}
94
101
id={
95
102
!props.preview ? elementId.block(props.entityID).container : undefined
103
103
+
}
104
104
+
onDragOver={
105
105
+
!props.preview && entity_set.permissions.write
106
106
+
? (e) => {
107
107
+
e.preventDefault();
108
108
+
e.stopPropagation();
109
109
+
}
110
110
+
: undefined
111
111
+
}
112
112
+
onDrop={
113
113
+
!props.preview && entity_set.permissions.write ? handleDrop : undefined
96
114
}
97
115
className={`
98
116
blockWrapper relative
+46
-25
components/Blocks/ImageBlock.tsx
···
51
51
}
52
52
}, [isSelected, props.preview, props.entityID]);
53
53
54
54
+
const handleImageUpload = async (file: File) => {
55
55
+
if (!rep) return;
56
56
+
let entity = props.entityID;
57
57
+
if (!entity) {
58
58
+
entity = v7();
59
59
+
await rep?.mutate.addBlock({
60
60
+
parent: props.parent,
61
61
+
factID: v7(),
62
62
+
permission_set: entity_set.set,
63
63
+
type: "text",
64
64
+
position: generateKeyBetween(
65
65
+
props.position,
66
66
+
props.nextPosition,
67
67
+
),
68
68
+
newEntityID: entity,
69
69
+
});
70
70
+
}
71
71
+
await rep.mutate.assertFact({
72
72
+
entity,
73
73
+
attribute: "block/type",
74
74
+
data: { type: "block-type-union", value: "image" },
75
75
+
});
76
76
+
await addImage(file, rep, {
77
77
+
entityID: entity,
78
78
+
attribute: "block/image",
79
79
+
});
80
80
+
};
81
81
+
54
82
if (!image) {
55
83
if (!entity_set.permissions.write) return null;
56
84
return (
···
65
93
${isSelected && !isLocked ? "border-2 border-tertiary font-bold" : "border border-border"}
66
94
${props.pageType === "canvas" && "bg-bg-page"}`}
67
95
onMouseDown={(e) => e.preventDefault()}
96
96
+
onDragOver={(e) => {
97
97
+
e.preventDefault();
98
98
+
e.stopPropagation();
99
99
+
}}
100
100
+
onDrop={async (e) => {
101
101
+
e.preventDefault();
102
102
+
e.stopPropagation();
103
103
+
if (isLocked) return;
104
104
+
const files = e.dataTransfer.files;
105
105
+
if (files && files.length > 0) {
106
106
+
const file = files[0];
107
107
+
if (file.type.startsWith('image/')) {
108
108
+
await handleImageUpload(file);
109
109
+
}
110
110
+
}
111
111
+
}}
68
112
>
69
113
<div className="flex gap-2">
70
114
<BlockImageSmall
···
79
123
accept="image/*"
80
124
onChange={async (e) => {
81
125
let file = e.currentTarget.files?.[0];
82
82
-
if (!file || !rep) return;
83
83
-
let entity = props.entityID;
84
84
-
if (!entity) {
85
85
-
entity = v7();
86
86
-
await rep?.mutate.addBlock({
87
87
-
parent: props.parent,
88
88
-
factID: v7(),
89
89
-
permission_set: entity_set.set,
90
90
-
type: "text",
91
91
-
position: generateKeyBetween(
92
92
-
props.position,
93
93
-
props.nextPosition,
94
94
-
),
95
95
-
newEntityID: entity,
96
96
-
});
97
97
-
}
98
98
-
await rep.mutate.assertFact({
99
99
-
entity,
100
100
-
attribute: "block/type",
101
101
-
data: { type: "block-type-union", value: "image" },
102
102
-
});
103
103
-
await addImage(file, rep, {
104
104
-
entityID: entity,
105
105
-
attribute: "block/image",
106
106
-
});
126
126
+
if (!file) return;
127
127
+
await handleImageUpload(file);
107
128
}}
108
129
/>
109
130
</label>
+11
components/Blocks/index.tsx
···
17
17
import { useEffect } from "react";
18
18
import { addShortcut } from "src/shortcuts";
19
19
import { QuoteEmbedBlock } from "./QuoteEmbedBlock";
20
20
+
import { useHandleDrop } from "./useHandleDrop";
20
21
21
22
export function Blocks(props: { entityID: string }) {
22
23
let rep = useReplicache();
···
231
232
}) => {
232
233
let { rep } = useReplicache();
233
234
let entity_set = useEntitySetContext();
235
235
+
let handleDrop = useHandleDrop({
236
236
+
parent: props.entityID,
237
237
+
position: props.lastRootBlock?.position || null,
238
238
+
nextPosition: null,
239
239
+
});
234
240
235
241
if (!entity_set.permissions.write) return;
236
242
return (
···
267
273
}, 10);
268
274
}
269
275
}}
276
276
+
onDragOver={(e) => {
277
277
+
e.preventDefault();
278
278
+
e.stopPropagation();
279
279
+
}}
280
280
+
onDrop={handleDrop}
270
281
/>
271
282
);
272
283
};
+74
components/Blocks/useHandleDrop.ts
···
1
1
+
import { useCallback } from "react";
2
2
+
import { useReplicache } from "src/replicache";
3
3
+
import { generateKeyBetween } from "fractional-indexing";
4
4
+
import { addImage } from "src/utils/addImage";
5
5
+
import { useEntitySetContext } from "components/EntitySetProvider";
6
6
+
import { v7 } from "uuid";
7
7
+
8
8
+
export const useHandleDrop = (params: {
9
9
+
parent: string;
10
10
+
position: string | null;
11
11
+
nextPosition: string | null;
12
12
+
}) => {
13
13
+
let { rep } = useReplicache();
14
14
+
let entity_set = useEntitySetContext();
15
15
+
16
16
+
return useCallback(
17
17
+
async (e: React.DragEvent) => {
18
18
+
e.preventDefault();
19
19
+
e.stopPropagation();
20
20
+
21
21
+
if (!rep) return;
22
22
+
23
23
+
const files = e.dataTransfer.files;
24
24
+
if (!files || files.length === 0) return;
25
25
+
26
26
+
// Filter for image files only
27
27
+
const imageFiles = Array.from(files).filter((file) =>
28
28
+
file.type.startsWith("image/"),
29
29
+
);
30
30
+
31
31
+
if (imageFiles.length === 0) return;
32
32
+
33
33
+
let currentPosition = params.position;
34
34
+
35
35
+
// Calculate positions for all images first
36
36
+
const imageBlocks = imageFiles.map((file) => {
37
37
+
const entity = v7();
38
38
+
const position = generateKeyBetween(
39
39
+
currentPosition,
40
40
+
params.nextPosition,
41
41
+
);
42
42
+
currentPosition = position;
43
43
+
return { file, entity, position };
44
44
+
});
45
45
+
46
46
+
// Create all blocks in parallel
47
47
+
await Promise.all(
48
48
+
imageBlocks.map((block) =>
49
49
+
rep.mutate.addBlock({
50
50
+
parent: params.parent,
51
51
+
factID: v7(),
52
52
+
permission_set: entity_set.set,
53
53
+
type: "image",
54
54
+
position: block.position,
55
55
+
newEntityID: block.entity,
56
56
+
}),
57
57
+
),
58
58
+
);
59
59
+
60
60
+
// Upload all images in parallel
61
61
+
await Promise.all(
62
62
+
imageBlocks.map((block) =>
63
63
+
addImage(block.file, rep, {
64
64
+
entityID: block.entity,
65
65
+
attribute: "block/image",
66
66
+
}),
67
67
+
),
68
68
+
);
69
69
+
70
70
+
return true;
71
71
+
},
72
72
+
[rep, params.position, params.nextPosition, params.parent, entity_set.set],
73
73
+
);
74
74
+
};