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