Skip to content

Adding a js confirmation before navigating away from settings menus#10927

Open
anukasha-mo wants to merge 9 commits intoWordPress:trunkfrom
anukasha-mo:64623-data-loss-prevention
Open

Adding a js confirmation before navigating away from settings menus#10927
anukasha-mo wants to merge 9 commits intoWordPress:trunkfrom
anukasha-mo:64623-data-loss-prevention

Conversation

@anukasha-mo
Copy link
Copy Markdown

@anukasha-mo anukasha-mo commented Feb 13, 2026

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 13, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props anukasha, westonruter, joedolson.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Comment on lines +34 to +49
/**
* Warn the user if they have unsaved changes.
*
* The browser will show a native confirmation dialog when the user
* attempts to leave the page with unsaved changes.
*/
$( window ).on( 'beforeunload', function() {
// Skip warning if form is being submitted or content hasn't changed.
if ( isSubmitting || ! $form || ! $form.length ) {
return;
}

if ( originalFormContent !== $form.serialize() ) {
return __( 'The changes you made will be lost if you navigate away from this page.' );
}
} );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break bfcache. The beforeunload should only be added once a field is modified. See https://web.dev/articles/bfcache#beforeunload-caution

For example, add a delegated event listener for the change event. For example:

document.addEventListener( 'change', () => {
    window.addEventListener( 'beforeunload', () => {
        // Check if the original form content is not the same as the current form content.
    } );
}, { once: true } );

}
} );

}( jQuery ) );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid using jQuery unless we have to.

return;
}

if ( originalFormContent !== $form.serialize() ) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use FormData as an alternative to this jQuery API.

'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
);

wp_enqueue_script( 'user-profile' );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not enqueue user-profile anymore?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do need to enqueue user-profile as well, re-added it


$scripts->add( 'language-chooser', "/wp-admin/js/language-chooser$suffix.js", array( 'jquery' ), false, 1 );

$scripts->add( 'options', "/wp-admin/js/options$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handle seems overly generic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like wp-admin-unsaved-changes-confirmation (although this may be overly un-generic! 😄).

Copy link
Copy Markdown
Author

@anukasha-mo anukasha-mo Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about wp-admin-options? Since we might be possibly moving other js hooked to admin_head here as well, keeping it a little generic..

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to wp-admin-unsaved-changes-confirmation since the js file is also now un-generic

Copy link
Copy Markdown
Contributor

@joedolson joedolson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to adding a new JS file that contains JS for options screens, but if so, I think we should be moving all the existing JS into it, as well. Currently, options.php contains a ton of admin_head hooked functions that print JS for the various pages, and we instead handle those differently in a central file.

@westonruter westonruter requested a review from Copilot April 9, 2026 08:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an “unsaved changes” confirmation dialog to WordPress admin Settings (“options-*”) pages to prevent accidental navigation away from modified forms.

Changes:

  • Registers a new options admin script with i18n support in core script loader.
  • Enqueues the new script across several Settings pages (general/reading/writing/discussion/media/permalink).
  • Introduces src/js/_enqueues/admin/options.js and wires it into the Grunt build pipeline.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

File Description
src/wp-includes/script-loader.php Registers the new admin script and translations.
src/wp-admin/options-*.php Enqueues the new script on Settings screens.
src/js/_enqueues/admin/options.js Implements dirty-form detection + beforeunload warning.
Gruntfile.js Builds and copies the new admin bundle output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
);

wp_enqueue_script( 'options' );
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enqueue is duplicated across multiple options-*.php screens in the PR. Consider centralizing the enqueue in an appropriate hook (e.g. admin_enqueue_scripts keyed off the current screen) to avoid repeating the same logic across many files and reduce the risk of future drift if the handle/deps change.

Copilot uses AI. Check for mistakes.
@anukasha-mo
Copy link
Copy Markdown
Author

anukasha-mo commented Apr 14, 2026

I'm not opposed to adding a new JS file that contains JS for options screens, but if so, I think we should be moving all the existing JS into it, as well. Currently, options.php contains a ton of admin_head hooked functions that print JS for the various pages, and we instead handle those differently in a central file.

@joedolson I agree with your point.
I have created a new ticket for moving all existing js to options.js - https://core.trac.wordpress.org/ticket/65070
Current ticket will simply introduce the options.js file and is scoped to the unsaved changes feature for now.

-----Edit------
After reviewing more, my thoughts:

Functions hooked to admin_head are loaded for specific options menu. For example, options_discussion_add_js callback function is hooked on admin_head in only options-discussion.php.
Adding this js to a common options.js would ideally not make any sense.

Hence, for now, I am renaming options.js to wp-options-unsaved-changes-confirmation.js

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1263 to +1264
$scripts->add( 'wp-admin-options', "/wp-admin/js/options$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 );
$scripts->set_translations( 'wp-admin-options' );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script is registered under the handle wp-admin-options, but the settings screens in this PR enqueue options. As-is, wp_enqueue_script( 'options' ) won't load anything and translations will also not be applied. Align the handle name (either register as options here, or update all enqueues to wp-admin-options) and keep it consistent with other admin script handles in this file.

Suggested change
$scripts->add( 'wp-admin-options', "/wp-admin/js/options$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 );
$scripts->set_translations( 'wp-admin-options' );
$scripts->add( 'options', "/wp-admin/js/options$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 );
$scripts->set_translations( 'options' );

Copilot uses AI. Check for mistakes.
'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
);

wp_enqueue_script( 'options' );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_enqueue_script( 'options' ) refers to a script handle that isn't registered in wp_default_scripts() in this PR (the added handle there is wp-admin-options). This means the confirmation script won't be loaded on this screen until the handle names are aligned.

Suggested change
wp_enqueue_script( 'options' );
wp_enqueue_script( 'wp-admin-options' );

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +18
event.preventDefault();
return __(
'The changes you made will be lost if you navigate away from this page.'
);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This beforeunload listener is added via addEventListener(), but the handler only returns a string. For beforeunload, the return value from an event listener is ignored in many browsers; you generally need to set event.returnValue (and typically still call preventDefault()) to reliably trigger the confirmation dialog. Update the handler to assign event.returnValue to the translated message (and return it if needed for older compat).

Suggested change
event.preventDefault();
return __(
'The changes you made will be lost if you navigate away from this page.'
);
var message = __(
'The changes you made will be lost if you navigate away from this page.'
);
event.preventDefault();
event.returnValue = message;
return message;

Copilot uses AI. Check for mistakes.

$scripts->add( 'language-chooser', "/wp-admin/js/language-chooser$suffix.js", array( 'jquery' ), false, 1 );

$scripts->add( 'wp-admin-options', "/wp-admin/js/options$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script's dependency list includes jquery, but options.js doesn't appear to use jQuery. Dropping the unused dependency avoids forcing jQuery onto the page and keeps the dependency graph accurate.

Suggested change
$scripts->add( 'wp-admin-options', "/wp-admin/js/options$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 );
$scripts->add( 'wp-admin-options', "/wp-admin/js/options$suffix.js", array( 'wp-i18n' ), false, 1 );

Copilot uses AI. Check for mistakes.
);

wp_enqueue_script( 'user-profile' );
wp_enqueue_script( 'options' );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_enqueue_script( 'options' ) refers to a script handle that isn't registered in wp_default_scripts() in this PR (the added handle there is wp-admin-options). This means the confirmation script won't be loaded on this screen until the handle names are aligned.

Suggested change
wp_enqueue_script( 'options' );
wp_enqueue_script( 'wp-admin-options' );

Copilot uses AI. Check for mistakes.
'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
);

wp_enqueue_script( 'options' );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_enqueue_script( 'options' ) refers to a script handle that isn't registered in wp_default_scripts() in this PR (the added handle there is wp-admin-options). This means the confirmation script won't be loaded on this screen until the handle names are aligned.

Suggested change
wp_enqueue_script( 'options' );
wp_enqueue_script( 'wp-admin-options' );

Copilot uses AI. Check for mistakes.
get_current_screen()->set_help_sidebar( $help_sidebar_content );
unset( $help_sidebar_content );

wp_enqueue_script( 'options' );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_enqueue_script( 'options' ) refers to a script handle that isn't registered in wp_default_scripts() in this PR (the added handle there is wp-admin-options). This means the confirmation script won't be loaded on this screen until the handle names are aligned.

Suggested change
wp_enqueue_script( 'options' );
wp_enqueue_script( 'wp-admin-options' );

Copilot uses AI. Check for mistakes.
'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
);

wp_enqueue_script( 'options' );
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_enqueue_script( 'options' ) refers to a script handle that isn't registered in wp_default_scripts() in this PR (the added handle there is wp-admin-options). This means the confirmation script won't be loaded on this screen until the handle names are aligned.

Suggested change
wp_enqueue_script( 'options' );
wp_enqueue_script( 'wp-admin-options' );

Copilot uses AI. Check for mistakes.

// Add the beforeunload listener only once a field is modified, to avoid
// breaking bfcache.
document.addEventListener( 'change', function () {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change listener is attached to document, so any change anywhere on the page (e.g. Screen Options / Help toggles) will register a beforeunload handler and can still disable bfcache even if the settings form itself was never modified. Consider attaching the { once: true } change listener to the settings form (or its inputs) instead, so the beforeunload handler is only registered when the options form changes.

Suggested change
document.addEventListener( 'change', function () {
form.addEventListener( 'change', function () {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants