johnlindquist / kit

Script Kit. Automate Anything.
https://scriptkit.com
MIT License
3.91k stars 138 forks source link

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

Open shyagamzo opened 1 year ago

shyagamzo commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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