kit
kit copied to clipboard
Drop on widget from VSCode doesn't pass-in dropped file path
Trying to produce a widget which enables dropping all kinds of stuff and passing them through a certain pipeline.
import '@johnlindquist/kit';
const render = () => md(`
# Drop anything here 🤩
{{ collected }}
`);
const dropZone = await widget(render(), {alwaysOnTop: true})
dropZone.onDrop(input =>
{
inspect(input);
});
When I drop from Windows Explorer or even from Total Commander, I get input.dataset.files
filled with an array of my dragged file/folder paths:
{
"dataset": {
"files": [
"C:\\.......complete-path\\wiki",
"C:\\.......complete-path\\pnpm-lock.yaml",
"C:\\.......complete-path\\package.json"
]
},
"targetId": "drop-anything-here-🤩",
"widgetId": "1682852262142",
"x": 1200,
"y": 0,
"width": 242,
"height": 120,
"pid": 380,
"channel": "WIDGET_DROP"
}
When dragging from VSCode, it doesn't pickup on the paths:
{
"dataset": {
"files": []
},
"targetId": "drop-anything-here-🤩",
"widgetId": "1682852262142",
"x": 1200,
"y": 0,
"width": 242,
"height": 120,
"pid": 380,
"channel": "WIDGET_DROP"
}
Any chance of getting this to work?
Found another clue...
await drop()
is more capable than widget.onDrop()
. It detects VSCode drags and even plain text drags (e.g. Chrome address bar, selection in Word, etc.).
The difference is, instead of the drag message object, it receives VSCode drags as a long plain string:
C:\some\path\to\file1.tsC:\some\path\to\file2.ts
I'm stuck...
drop
won't let me display my own html, and the widget doesn't grab all drop types... 🥲
Need help 🙏
Found a workaround... 💪😪 While debugging the widget, I discovered how it handles the drag event. After a long trail and error I implemented my own handler and planted it in my HTML (the one I give the widget when I create it).
This creates a second handler to the browser's drag event and handles it differently:
- It uses the
items
array instead of thefiles
array, to produce more data regarding the dragged item. - It maps and categories the items into six categories: text, html, uri, image, file and other.
- It sends the new categories data over the same IPC channel, but uses a different property.
<script>
const mappers = {
string: {
'text/plain': 'text',
'text/html': 'html',
'text/uri-list': 'uri',
read: (item) => new Promise(item.getAsString.bind(item))
},
file: {
'image': 'image',
'': 'file',
read: (item) => Promise.resolve(fileToJSON(item.getAsFile()))
}
};
// Because we can't send a File object to the main process, we need to convert it to a JSON object.
// JSON.stringify didn't work as the properties are not enumerable.
// Had to use this trick.
function fileToJSON({ name, path, size, type, lastModified, lastModifiedDate, webkitRelativePath })
{
return { name, path, size, type, lastModified, lastModifiedDate, webkitRelativePath };
}
document.addEventListener("drop", async (event) =>
{
event.preventDefault();
let { id = "" } = event.target.closest("*[id]");
const { items } = event.dataTransfer;
// Resolve all items into a big array of { category, info, mime }
const data = await Promise.all(
Array.from(items).map(async (item) =>
{
const mapper = mappers[item.kind]; // file or string
const mime = item.type; // text/plain image/png etc
const type = Object.keys(mapper).find(key => mime.startsWith(key));
return {
category: mapper?.[type] ?? 'other',
info: await (mapper?.read ?? mappers.string.read)(item),
mime: mime === '' ? undefined : mime
};
}, [])
);
// Group by category
const dataByType = data.reduce((acc, { category, info, mime }) =>
{
(acc[category] ??= []).push({ info, mime });
return acc;
}, {});
// Send items to main process on the `dataTransfer` property, which is different to the `dataset` property
// produced by ScriptKit. This allows me to ignore Kit's drop messages quickly and use this one.
ipcRenderer.send("WIDGET_DROP", {
dataTransfer: dataByType,
targetId: id,
widgetId: window.widgetId,
});
});
</script>
Now my widget can handle all kinds of dragged items:
https://user-images.githubusercontent.com/95415447/235792958-593b06c8-63b2-4b80-b491-fdf5aaf979f4.mp4