kit icon indicating copy to clipboard operation
kit copied to clipboard

Drop on widget from VSCode doesn't pass-in dropped file path

Open shyagamzo opened this issue 1 year ago • 2 comments

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?

shyagamzo avatar Apr 30 '23 11:04 shyagamzo

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 🙏

shyagamzo avatar May 02 '23 06:05 shyagamzo

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:

  1. It uses the items array instead of the files array, to produce more data regarding the dragged item.
  2. It maps and categories the items into six categories: text, html, uri, image, file and other.
  3. 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

shyagamzo avatar May 02 '23 21:05 shyagamzo