Electron Adventures: Episode 92: Dock Drag and Drop
Tags: javascript, electron, svelte, osx
Published: 2021-11-09 16:05:36

I wanted to be able to drag and drop CSV files from Finder onto the dock icon for the app to open them.

The first issue this runs into is that such integrations only work if we tell OSX about them through Info.plist, and that's only possible for packaged apps, so I need to go through all the steps from episode 80 for packaging Electron Svelte app.

### `Info.plist`

First, let's create `Info.plist`. I'm not really sure how these work, but I found some examples online, and replaced whatever was in them with `CSV` and `text/csv` to tell the system these are the file types we support.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDocumentTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeExtensions</key>
      <array>
        <string>csv</string>
      </array>
      <key>CFBundleTypeIconFile</key>
      <string>electron.icns</string>
      <key>CFBundleTypeMIMETypes</key>
      <array>
        <string>text/csv</string>
      </array>
      <key>CFBundleTypeName</key>
      <string>CSV file</string>
      <key>CFBundleTypeOSTypes</key>
      <array>
        <string>CSV</string>
      </array>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
    </dict>
  </array>
</dict>
</plist>
```

### `package.json`

```javascript
{
  "name": "episode-92-dock-drag-and-drop",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public --no-clear",
    "electron": "electron .",
    "forge-start": "electron-forge start",
    "package": "electron-forge package",
    "make": "electron-forge make"
  },
  "devDependencies": {
    "@electron-forge/cli": "^6.0.0-beta.61",
    "@electron-forge/maker-deb": "^6.0.0-beta.61",
    "@electron-forge/maker-rpm": "^6.0.0-beta.61",
    "@electron-forge/maker-squirrel": "^6.0.0-beta.61",
    "@electron-forge/maker-zip": "^6.0.0-beta.61",
    "@rollup/plugin-commonjs": "^17.0.0",
    "@rollup/plugin-node-resolve": "^11.0.0",
    "electron": "^13.1.8",
    "rollup": "^2.3.4",
    "rollup-plugin-css-only": "^3.1.0",
    "rollup-plugin-livereload": "^2.0.0",
    "rollup-plugin-svelte": "^7.0.0",
    "rollup-plugin-terser": "^7.0.0",
    "svelte": "^3.0.0"
  },
  "dependencies": {
    "d3-dsv": "^3.0.1",
    "electron-log": "^4.4.1",
    "electron-settings": "^4.0.2",
    "electron-squirrel-startup": "^1.0.0",
    "sirv-cli": "^1.0.0"
  },
  "config": {
    "forge": {
      "packagerConfig": {
        "extendInfo": "Info.plist"
      },
      "makers": [
        {
          "name": "@electron-forge/maker-squirrel",
          "config": {
            "name": "episode_92_dock_drag_and_drop"
          }
        },
        {
          "name": "@electron-forge/maker-zip",
          "platforms": [
            "darwin"
          ]
        },
        {
          "name": "@electron-forge/maker-deb",
          "config": {}
        },
        {
          "name": "@electron-forge/maker-rpm",
          "config": {}
        }
      ]
    }
  }
}
```

For `package.json` we need to follow the same steps as back in episode 80, but then we also need to add the `config.forge.packagerConfig.extendInfo` property to tell the packager to use the `Info.plist` file.

### `index.js`

```javascript
let { app, BrowserWindow, dialog, Menu } = require("electron")
let settings = require("electron-settings")
let log = require("electron-log")

let isOSX = (process.platform === "darwin")

function createWindow(path) {
  log.info("Creating window for", path)
  let key = `windowState-${path}`
  let windowState = settings.getSync(key) || { width: 1024, height: 768 }

  let qs = new URLSearchParams({ path }).toString()
  let win = new BrowserWindow({
    ...windowState,
    webPreferences: {
      preload: `${__dirname}/preload.js`,
    },
  })

  function saveSettings() {
    windowState = win.getBounds()
    log.info("Saving window position", path, windowState)
    settings.setSync(key, windowState)
  }

  win.on("resize", saveSettings)
  win.on("move", saveSettings)
  win.on("close", saveSettings)

  if (app.isPackaged) {
    win.loadFile(`${__dirname}/public/index.html`, {query: {path}})
  } else {
    win.loadURL(`http://localhost:5000/?${qs}`)
  }
}

async function openFiles() {
  let { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ["openFile", "multiSelections", "showHiddenFiles"],
    filters: [
      { name: "CSV files", extensions: ["csv"] },
      { name: "All Files", extensions: ["*"] }
    ],
    message: "Select a CSV file to open",
    defaultPath: `${__dirname}/samples`,
  })
  if (canceled && !isOSX) {
    app.quit()
  }
  for (let path of filePaths) {
    createWindow(path)
  }
}

let dockMenu = Menu.buildFromTemplate([
  {
    label: "Open files",
    click() { openFiles() }
  }
])

async function startApp() {
  if (isOSX) {
    app.dock.setMenu(dockMenu)
  }
  await openFiles()
  if (isOSX) {
    app.on("activate", function() {
      if (BrowserWindow.getAllWindows().length === 0) {
        openFiles()
      }
    })
  }
}

app.on("window-all-closed", () => {
  if (!isOSX) {
    app.quit()
  }
})

app.on("ready", startApp)

app.on("open-file", (event, path) => {
  log.info("Opening file through drag and drop to Dock", path)
  createWindow(path)
})
```

This file needed two changes. First, the change we actually want - a handler for `open-file` event. It gets passed `path` as the second argument, and we just use that to open a new window.

The other change is that we want to pass query string to the app, regardless of if it's in packaged or development mode. Honestly I find it embarassing that Electron doesn't just have a builtin way to do this without switching logic, but this works:

```javascript
  if (app.isPackaged) {
    win.loadFile(`${__dirname}/public/index.html`, {query: {path}})
  } else {
    let qs = new URLSearchParams({ path }).toString()
    win.loadURL(`http://localhost:5000/?${qs}`)
  }
```

### Application logs

All the events are logged to `~/Library/Logs/episode-92-dock-drag-and-drop/main.log`, so you can see such wonderful messages as:

```
[2021-11-09 13:55:05.520] [info]  Opening file through drag and drop to Dock /Users/taw/electron-adventures/episode-92-dock-drag-and-drop/samples/07-lover.csv
[2021-11-09 13:55:05.521] [info]  Creating window for /Users/taw/electron-adventures/episode-92-dock-drag-and-drop/samples/07-lover.csv
```

### Results

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-92-dock-drag-and-drop).

For the next episode, we'll see if we can get Opal Ruby working with Electron.