Skip to content

Instantly share code, notes, and snippets.

@leleogere
Last active March 5, 2025 21:28
Show Gist options
  • Select an option

  • Save leleogere/0d021d26bde8ff5fba3006c1d0c309fb to your computer and use it in GitHub Desktop.

Select an option

Save leleogere/0d021d26bde8ff5fba3006c1d0c309fb to your computer and use it in GitHub Desktop.
Warn before closing a GitHub new issue tab

GitHub issue close confirmation dialog

Description

This GreaseMonkey/TamperMonkey/ViolentMonkey script aims at solving the issue raised in https://github.com/orgs/community/discussions/42581, i.e. closing an unsubmitted GitHub issue, and losing quite a lot of work.

This will display a confirmation popup before closing a tab with an unsubmitted issue, so that you don't close it by mistake.

I've tested it with Firefox (the custom message does not work due to this, but I left it as it might work for other browsers) and ViolentMonkey.

You should be able to use this link to install the script, provided that you have a UserScript manager such as GreaseMonkey/TamperMonkey/ViolentMonkey.

IMPORTANTE NOTE: The way GitHub navigates between pages makes this script more complex than it should be (see version history and discution with @paponius below), that's why now the script constantly monitors if the URL matches the new issue URL template rather than relying on browser for notifying a page change.

Changelog

v1.6

Fix a typo and improve confirmation message.

v1.5

Changed implementation to be more robust with how GitHub manages page changes (thanks @paponius). Now, the script query the URL every 60 seconds on the whole GitHub website, and only triggers when it detects that it matches the URL of a new issue. Note that this has the downside of not displaying the popup if you close the tab before the first check (in the worst case 60 seconds). I guess that no one would have the time to write anything that valuable in 60 seconds, and I wanted to keep this interval high to avoid consumming too much ressources (as it runs on any GitHub page).

v1.4

Implemented some @paponius suggestions:

  • Changed the title and body selectors so that they do not depend on unstable IDs
  • Changed the extension to .user.js for better integration with UserScript managers
  • Added a 1s delay before getting the elements as sometimes they would not be loaded yet when the script starts (and the selectors would return null)

v1.2

  • Changed title and body ID from issue_title and issue_body to :r1: and :r6:.
// ==UserScript==
// @name Confirm before closing unsubmitted GitHub issues
// @author leleogere
// @description Warn before closing a tab with an unsubmitted GitHub issue
// @version 1.6
// @match https://github.com/*
// ==/UserScript==
// check the URL every 60s to see if it matches the new issue URL
setInterval( () => {
'use strict';
// wait 3s for "page change" to settle
setTimeout( () => {
// early exit
if (!window.location.pathname.includes('/issues/new')) { return; };
let hasFormBeenModified = false;
// Selectors for input fields on the issue form
const titleSelector = 'input[class*="TextInput"]';
const bodySelector = 'textarea[class*="Textarea"]';
// Monitor input fields for changes
const monitorChanges = () => {
const titleField = document.querySelector(titleSelector);
const bodyField = document.querySelector(bodySelector);
// console.log(titleField)
// console.log(bodyField)
if (titleField && bodyField) {
titleField.addEventListener('input', () => {
hasFormBeenModified = true;
});
bodyField.addEventListener('input', () => {
hasFormBeenModified = true;
});
}
};
// Warn user when trying to close the tab
window.addEventListener('beforeunload', (event) => {
if (hasFormBeenModified) {
const message = 'You have a saved draft of an unsubmitted Issue, which you can still recover using "New Issue" button (in this tab). Closing this tab will remove the draft.';
event.returnValue = message; // Standard for older browsers
return message; // For modern browsers
}
});
monitorChanges();
}, 3000);
}, 60000);
@paponius
Copy link

paponius commented Feb 28, 2025

With various repos, it was always something else.
:r1l:, :r6:, :r1q
but it's not even "1" and "6" respectively. Once I observed: title: :r1l: and comment :r1q:

I did this:

    const titleSelector = 'input[class*="TextInput"]';
    const commentSelector = 'textarea[class*="Textarea"]';
  1. add .user. to the filename, so it can be installed and auto-updated from this gist comfortably.
    This would then be a stable link to click (with installed GM/TP/VM):
    https://gist.github.com/leleogere/0d021d26bde8ff5fba3006c1d0c309fb/raw/confirm_before_closing_github_issue.user.js

  2. It might happen that load event is missed. (with some versions of some UserScript managers), maybe would be safer to check document.readyState === 'complete' (I use this)
    edit: TM doc says both are cached and delivered when UserScript is ready to receive them. I tested it today. It seems to be an issue of the past. Disregard the 3. point.

@leleogere
Copy link
Author

@paponius thank for your feedback!

  1. As the IDs I had on the repo I checked was the same as yours, I thought that it would be the same for all repos, but it seems that it's not the case. I implemented your solution and it works well!
  2. Good idea, I did not know that, it's quite handy!
  3. I had some trouble with sometimes the selectors returning null, probably because the elements hadn't been loaded yet at the script start. I do not know if it is linked to that load event you mentioned (probably not as you said that this was supposed to be fixed), but I added a 1s delay before getting the elements and it fixes it.

@paponius
Copy link

paponius commented Mar 3, 2025

I did not observe null. Must be ReactJS doing its weird stuff.
There is a parent div[data-target="react-app.reactRoot"], you could put an Observer on it, as well as on the BODY, then on each removal/re-addition of child sub-tree register your listener again. Quite an overkill. If you are interested how and why, check comments in my "add size column to github".

But why? Maybe even set the setTimeout to 20 sec and call it a feature.
No one can write much worth saving in 20 sec. And if they try to close the Tab as soon, probably they typed something by mistake or changed their mind.

A suggestion to README.md:

Use this link to istall
Don't click on RAW to install, as that would install a specific commit and disallow future updates from *Monkey.

@leleogere
Copy link
Author

Yeah, I think the timeout is OK, no need for an observer. I left it at 1s, this is probably OK to raise it to 20s as you said but it works fine like that.

Nice suggestion, I added the link.

@paponius
Copy link

paponius commented Mar 3, 2025

I installed it, tested and realized, this is not as simple with React env.

The thing is, the github.com page is loaded only once per overall browsing. Every navigation to a link will just rewrite either part of a page, or the whole content of the BODY. It then updates Omnibox manually to simulate native page change. The problem is, with the @match https://github.com/*/*/issues/new* and entering the github somewhere else on GitHub.com, the UserScript is never injected.

So, it can't be done without an Observer after all. And I don't consider permanent pinging of URL or querying DOM periodically as a solution.

@leleogere
Copy link
Author

Yes you're right... Unfortunately, this begins to get beyond my skills in JS, feel free to create your own script, I'll link it here if you want!

@paponius
Copy link

paponius commented Mar 3, 2025

I tired navigate event, with no success,
tried to mod history.replaceState and pushState, which GitHub might be using. nothing.

Then DOM observers. But the React always does something else. There are multiple points in DOM where it removes and adds stuff, during load. Even after it's done, will just remove something for a split of a second only. That's a problem as it removes Observers. Need Observers to watch and redo other Observers.

And then always something else is needed to watch, depending on which page the browsing starts. i.e. github.com/, or repo root, or on page ending .../issues, or directly /issues/new.

I gave up on following all DOM changes and just used timer, but 1s was too much for fast clicking, now 500ms seems fine.
Not sure if it covers all cases. Just tried to start (F5) on repo root and /Issues pages. And it seems to work.

Both these experiments are here

Of course solutions like permanent setInterval to check window.location, or Observer on BODY with change to all children are possible. I just wouldn't like that.

@leleogere
Copy link
Author

Yeah there do not seem to be an easy solution. A workaround seems to manually refresh the page when starting a new issue. Like that, the script is run as expected. However, this means that you have to remember to do it.

@paponius
Copy link

paponius commented Mar 4, 2025

I think most would just do setInterval with document.querySelector(), which is not nice, that's why browsers started to freeze background tabs. Too many of these easy solutions were eating up resources. Observer on BODY on a React page would be the worst, slowing down each page change. But 10sec setInterval with windows.location is not as bad. it's just to read one variable, not querying the whole DOM. If it's on top of script, it would be done real fast.

The version with observers would execute just couple times per page change. Then remain silent. But it's challenging, not just to make it work,
but to maintain it as every change on GitHub.com would break it.
This is where I put it together for test.
It seems to work, but probably needs to add more observers for more cases.

But just add something like setInterval(() => { if (!window.location.includes('issues/new') { return; } }, 10000) change @match to root of github.com. Should be fine.

@paponius
Copy link

paponius commented Mar 4, 2025

that return would do nothing of course. bad example...
here it is fully together: https://gist.github.com/paponius/d6642876d118870dde005441071b4ab5

There is one more situation, if you start writing the Issue, then navigate back in browser, you get the warning,
you say "leave", then you return to the issue/new, GitHub will recover it, then you don't type anything and close.
You'll not get another warning. But it's an edge case. Whatever.

@leleogere
Copy link
Author

leleogere commented Mar 4, 2025

I updated the script with your last implementation. I raised the URL monitor interval to 60s to limit resources even more. I think that it's ok to lose (at worst) 60s of work in case of mistake (which could be recovered by unclosing the tab as you suggested anyway).

There are always edge cases, but I think that's better than nothing.

I've found another issue though: on some repos (such as https://github.com/ActiveState/code/issues), the "New issue" button opens a popup rather than a new page, so the URL does not change. But supporting that would make the script more complex (as it would need to check for elements rather than simply the URL).

@paponius
Copy link

paponius commented Mar 4, 2025

There was a typo incudes and missing pathname, I updated it and actually tested this time. Same link.

yes, there is no /new, bummer.

and another,
when you navigate from issues/new somewhere else, but still on github, you'll not get warning, as the warning is tight up to closing a real page, not React "web page".
plus, you'll get the warning later, trying to close a page which is already other than issues/new.

edit: but that's actually good. Just change the warning, detect with window.location... again, based on location use the current message, or a message saying "there is a saved draft of your un-submitted Issue, which you can still recover using "New Issue" button. Closing this tab will remove the draft."

@leleogere
Copy link
Author

I fixed the typo and modified the message. However, note that on some browsers (such as Firefox), that message won't be displayed, and a generic confirmation dialog will pop up instead (see this discussion).

@paponius
Copy link

paponius commented Mar 5, 2025

yes, no browser will show it today. Not just Firefox.
I meant two different messages, based on the page the user is on when trying to close, but it's irrelevant as it's ignored.
I tried to inject the message to DOM, using two events, but that's also not possible.

I believe this is the best that can be done using beforeunload event. I am leaving it at that. Still better than nothing. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment