javascriptelectronframedraggabletitlebar

Is it possible to add an eventListener to a custom title bar used for dragging the window in a frameless window?


Added a custom title bar to a frameless window in order to make the window draggable. The title bar shows, but eventListener won't fire:

main.js:

const createWindow = () => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      frame: false,
      scrollbar: false,
      webPreferences:{
        nodeIntegration: true,
        contextIsolation: false,
      }
    })
    win.loadFile('index.html')
  }

index.html:

<body>
  <div class="window">
    <div class="window-content">
      <textarea>some content</textarea>
    </div>
  </div>
  <script src="index.js"></script>
</body>

index.js:

let titleBar = document.createElement('div')
titleBar.style.width = "100%"
titleBar.style.height = "32px"
titleBar.style.backgroundColor = "#fff"
titleBar.style.position = "absolute"
titleBar.style.top = titleBar.style.left = 0
titleBar.style.webkitAppRegion = "drag"
titleBar.textContent = 'My App';

document.body.appendChild(titleBar)

// Nothing happens
titleBar.addEventListener("mouseover", () => {
    console.log("hello")
})

Solution

  • Yes, it is possible to add an eventListener to a custom title bar used for window dragging in a frameless window, but it requires implementation of own mouse event code for dragging the window.

    The issue with mouse events not triggering the eventListener is caused by

    titleBar.style.webkitAppRegion = "drag"
    

    which interferes with the mechanism of propagating the events and prevents the mouse events to be detected by the listener.

    In other words it seems that in Electron you can't have both at the same time for same DOM-object: dragging using .style.webkitAppRegion = "drag" and receiving of the mouse events using .addEventListener("mouseover",. If you want the dragging behavior you need to implement it yourself using the from the DOM-object received mouse events.

    To see it yourself comment or remove the "drag" line and experience that the mouse event is then working as expected ( don't forget that the console output is provided after Ctrl+Shift+i in the window of the application and not in the Terminal window running the application ).

    Please be aware that using some "programming tricks" you can usually achieve any desired effect in any software system, so statements that something is not possible are generally not really correct from this perspective stating only that there is no straightforward simple and well known way of achieving something.

    A possible workaround is to have two title bars (or a dragging icon next to the title bar in same line): one for dragging the window and the other one for responding to mouse events:

    console.log("renderer.js loaded");
    
    document.addEventListener('DOMContentLoaded', () => {
        // Create the draggable title bar at the very top
        let draggableTitleBar = document.createElement('div');
        draggableTitleBar.style.width = "100%";
        draggableTitleBar.style.height = "24px";
        draggableTitleBar.style.backgroundColor = "#e74c3c";
        draggableTitleBar.style.position = "absolute";
        draggableTitleBar.style.top = "0px";
        draggableTitleBar.style.left = "0px";
        draggableTitleBar.style.zIndex = "1000";
        draggableTitleBar.style.webkitAppRegion = "drag";
        draggableTitleBar.textContent = 'Draggable Title Bar';
    
        // Create the non-draggable title bar below it
        let nonDraggableTitleBar = document.createElement('div');
        nonDraggableTitleBar.style.width = "100%";
        nonDraggableTitleBar.style.height = "40px";
        nonDraggableTitleBar.style.backgroundColor = "#3498db";
        nonDraggableTitleBar.style.position = "absolute";
        nonDraggableTitleBar.style.top = "24px"; // Adjust position as needed
        nonDraggableTitleBar.style.left = "0px";
        nonDraggableTitleBar.style.zIndex = "1000";
        nonDraggableTitleBar.textContent = 'Non-Draggable Title Bar';
    
        // Add event listeners to the non-draggable title bar
        nonDraggableTitleBar.addEventListener("mouseover", () => {
            console.log("Mouseover event triggered on non-draggable title bar");
        });
    
        nonDraggableTitleBar.addEventListener("click", () => {
            console.log("Non-draggable title bar clicked");
        });
    
        // Append both title bars to the document body
        document.body.appendChild(draggableTitleBar);
        document.body.appendChild(nonDraggableTitleBar);
    
        console.log("Title bars added to the DOM");
    });
    

    Here how it looks like:

    AppWindow with devTools Console

    For the sake of completeness below code of an application which uses the title bar for both: dragging the window and reception of mouse events (logged to the console):

    
    ~ $ cat package.json 
    {
        "name": "oOosysElectronApp",
        "version": "1.0.0",
        "main": "main.js",
        "scripts": {
            "start": "electron ."
        },
        "keywords": [],
        "author": "oOosys",
        "license": "---",
        "description": "PureGraphicWindowArea",
        "dependencies": {
            "electron": "^31.1.0"
        }
    }
    ~ $ cat main.js
    const { app, BrowserWindow, ipcMain } = require('electron');
    // const path = require('path');
    
    function createWindow() {
        const win = new BrowserWindow({
            width: 800,
            height: 480,
            frame: false,
            scrollbar: false,
            webPreferences: {
                nodeIntegration: true,
                contextIsolation: false,
            }
        });
    
        win.loadFile('index.html');
    
        ipcMain.on('get-window-position', (event) => {
            event.returnValue = win.getPosition();
        });
    
        ipcMain.on('set-window-position', (event, x, y) => {
        // console.log("set-window-position to x,y : ", x , y) // prints to TERMINAL 
            win.setPosition(x, y);
        });
    }
    
    app.whenReady().then(createWindow);
    
    app.on('window-all-closed', () => {
        if (process.platform !== 'darwin') {
            app.quit();
        }
    });
    
    app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
    ~ $ cat renderer.js
    const { ipcRenderer } = require('electron');
    
    console.log("renderer.js loaded");
    
    document.addEventListener('DOMContentLoaded', () => {
        // Create the title bar
        let titleBar = document.createElement('div');
        titleBar.style.width = "100%";
        titleBar.style.height = "32px";
        titleBar.style.backgroundColor = "lightblue"; // Temporary background color for visibility
        titleBar.style.position = "absolute";
        titleBar.style.top = "0px";
        titleBar.style.left = "0px";
        titleBar.style.zIndex = "1000"; // Ensure the title bar is on top
        titleBar.textContent = 'Window Dragging Title';
    
        document.body.appendChild(titleBar);
        console.log("Title bar added to the DOM");
    
        let isDragging = false;
        let startX, startY;
        let startWindowX, startWindowY;
    
        titleBar.addEventListener("mousedown", (event) => {
            event.preventDefault();
            event.stopPropagation();
            isDragging = true;
            startX = event.clientX;
            startY = event.clientY;
            const [windowX, windowY] = ipcRenderer.sendSync('get-window-position');
            startWindowX = windowX;
            startWindowY = windowY;
            console.log("MouseDOWN windowX,Y, mouseX,Y:", startWindowX, startWindowY, startX, startY);
        });
    
        // mousemove event interferes somehow with 
        //          ipcRenderer.send('set-window-position', newWindowX, newWindowY);
        //      causing the window to resize instead of re-position
        // In other words CONTINUOUS DRAGGING of the window FAILS TO WORK as expected
        
        document.addEventListener("mouseup", (event) => {
            if ( isDragging ) {
                const currentX = event.clientX;
                const currentY = event.clientY;
                const deltaX = currentX - startX;
                const deltaY = currentY - startY;
                const newWindowX = startWindowX + deltaX;
                const newWindowY = startWindowY + deltaY;
                ipcRenderer.send('set-window-position', newWindowX, newWindowY);
                console.log("mouseUP newWindowX,Y mouseMovedDistXY:", newWindowX, newWindowY, deltaX, deltaY);
                isDragging = false;
            }
        });
    });
    ~ $ cat index.html
    <!DOCTYPE html>
    <html><head><title>oOosys Electron App</title>
    <style>
        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
             background-color: #d0eed0;
        }
    </style></head>
    <body>
        <script src="renderer.js"></script>
        
        <br><br><br><br><br><br><br><br><br>
        <textarea>some content</textarea>
    </body></html>
    ~ $ npm start 2>/dev/null
    
    > oOosysElectronApp@1.0.0 start
    > electron .
    

    And here how it looks like after the code documented in the log of a shell session provided above is executed:

    movedWindow

    Please notice that the window moves to the target position on mouseup event. The mousemove event seems to interfere with the functionality of ipcRenderer.send('set-window-position', newWindowX, newWindowY); resulting in resizing the window instead of moving so this approach seems not be able to provide smooth continuous dragging of the window due to an issue with Electron functionality.