diff --git a/Gruntfile.js b/Gruntfile.js index 5f9109fac3cb0..fab5b76caad2c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -450,6 +450,7 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/site-health.js' ]: [ './src/js/_enqueues/admin/site-health.js' ], [ WORKING_DIR + 'wp-admin/js/site-icon.js' ]: [ './src/js/_enqueues/admin/site-icon.js' ], [ WORKING_DIR + 'wp-admin/js/privacy-tools.js' ]: [ './src/js/_enqueues/admin/privacy-tools.js' ], + [ WORKING_DIR + 'wp-admin/js/settings.js' ]: [ './src/js/_enqueues/admin/settings.js' ], [ WORKING_DIR + 'wp-admin/js/theme-plugin-editor.js' ]: [ './src/js/_enqueues/wp/theme-plugin-editor.js' ], [ WORKING_DIR + 'wp-admin/js/theme.js' ]: [ './src/js/_enqueues/wp/theme.js' ], [ WORKING_DIR + 'wp-admin/js/updates.js' ]: [ './src/js/_enqueues/wp/updates.js' ], @@ -1193,6 +1194,7 @@ module.exports = function(grunt) { 'src/wp-admin/js/theme.js': 'src/js/_enqueues/wp/theme.js', 'src/wp-admin/js/updates.js': 'src/js/_enqueues/wp/updates.js', 'src/wp-admin/js/user-profile.js': 'src/js/_enqueues/admin/user-profile.js', + 'src/wp-admin/js/settings.js': 'src/js/_enqueues/admin/settings.js', 'src/wp-admin/js/user-suggest.js': 'src/js/_enqueues/lib/user-suggest.js', 'src/wp-admin/js/widgets/custom-html-widgets.js': 'src/js/_enqueues/wp/widgets/custom-html.js', 'src/wp-admin/js/widgets/media-audio-widget.js': 'src/js/_enqueues/wp/widgets/media-audio.js', diff --git a/src/js/_enqueues/admin/settings.js b/src/js/_enqueues/admin/settings.js new file mode 100644 index 0000000000000..0fb8232902156 --- /dev/null +++ b/src/js/_enqueues/admin/settings.js @@ -0,0 +1,50 @@ +/** + * Warns users about unsaved changes on settings pages. + * + * @output wp-admin/js/settings.js + * @since 7.1.0 + */ + +/* global wp */ +( function( $ ) { + var __ = wp.i18n.__; + + // Target only the main settings form, not search or other forms. + var $form = $( 'form[action="options.php"]' ); + var originalData; + var isSubmitting = false; + + /** + * Attaches the beforeunload listener. Called once on the first user + * change so that bfcache is not blocked on pages with no edits. + */ + function startWatchingForUnload() { + // Remove this as a one-shot listener. + $form.off( 'change.settings input.settings', startWatchingForUnload ); + + $( window ).on( 'beforeunload.settings', function() { + if ( ! isSubmitting && originalData !== $form.serialize() ) { + return __( 'The changes you made will be lost if you navigate away from this page.' ); + } + } ); + } + + $( function() { + if ( ! $form.length ) { + return; + } + + // Snapshot the original form state. + originalData = $form.serialize(); + + // Suppress the warning when the form is intentionally submitted (settings saved). + $form.on( 'submit.settings', function() { + isSubmitting = true; + $( window ).off( 'beforeunload.settings' ); + } ); + + // Attach the beforeunload listener lazily on the first user interaction + // to preserve bfcache for pages where no changes are made. + $form.on( 'change.settings input.settings', startWatchingForUnload ); + } ); +} )( jQuery ); diff --git a/src/wp-admin/options-general.php b/src/wp-admin/options-general.php index 969065b7008f4..6fbcd83d04e57 100644 --- a/src/wp-admin/options-general.php +++ b/src/wp-admin/options-general.php @@ -249,9 +249,10 @@ class=""
want your site home page to be different from your WordPress installation directory.' ), - __( 'https://developer.wordpress.org/advanced-administration/server/wordpress-in-directory/' ) + /* translators: 1: Documentation URL. 2: Accessibility text (do not translate). */ + __( 'Enter the same address here unless you want your site home page to be different from your WordPress installation directory%2$s.' ), + esc_url( __( 'https://developer.wordpress.org/advanced-administration/server/wordpress-in-directory/' ) ), + ' ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '' ); ?>
@@ -568,8 +569,14 @@ class="" '' . __( 'Preview:' ) . ' ' . date_i18n( get_option( 'time_format' ) ) . '' . "\n" . '
'; - echo "\t" . __( 'Documentation on date and time formatting.' ) . "
\n"; -?> + printf( + "\t\n", + 'https://wordpress.org/documentation/article/customize-date-and-time-format/', + __( 'Documentation on date and time formatting' ), + /* translators: Hidden accessibility text. */ + __( '(opens in a new tab)' ) + ); + ?> diff --git a/src/wp-admin/options-head.php b/src/wp-admin/options-head.php index c951b77419505..90edab87013cb 100644 --- a/src/wp-admin/options-head.php +++ b/src/wp-admin/options-head.php @@ -21,3 +21,5 @@ } settings_errors(); + +wp_enqueue_script( 'settings' ); diff --git a/src/wp-admin/options-permalink.php b/src/wp-admin/options-permalink.php index 046c237b6a948..1fb2c92722fa0 100644 --- a/src/wp-admin/options-permalink.php +++ b/src/wp-admin/options-permalink.php @@ -222,9 +222,10 @@number of tags are available, and here are some examples to get you started.' ), - __( 'https://wordpress.org/documentation/article/customize-permalinks/' ) + /* translators: 1: Documentation URL. 2: Accessibility text (do not translate). */ + __( 'WordPress offers you the ability to create a custom URL structure for your permalinks and archives. Custom URL structures can improve the aesthetics, usability, and forward-compatibility of your links. A number of tags are available%2$s, and here are some examples to get you started.' ), + esc_url( __( 'https://wordpress.org/documentation/article/customize-permalinks/' ) ), + ' ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '' ); ?>
diff --git a/src/wp-admin/options-privacy.php b/src/wp-admin/options-privacy.php index 4205967acb3a8..e2ebb50215fd3 100644 --- a/src/wp-admin/options-privacy.php +++ b/src/wp-admin/options-privacy.php @@ -215,19 +215,23 @@ static function ( $body_class ) { ?> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . ''; + if ( 'publish' === get_post_status( $privacy_policy_page_id ) ) { printf( - /* translators: 1: URL to edit Privacy Policy page, 2: URL to view Privacy Policy page. */ - __( 'Edit or view your Privacy Policy page content.' ), + /* translators: 1: URL to edit Privacy Policy page, 2: URL to view Privacy Policy page, 3: Accessibility text (do not translate). */ + __( 'Edit or view%3$s your Privacy Policy page content.' ), esc_url( $edit_href ), - esc_url( $view_href ) + esc_url( $view_href ), + $new_tab_indicator ); } else { printf( - /* translators: 1: URL to edit Privacy Policy page, 2: URL to preview Privacy Policy page. */ - __( 'Edit or preview your Privacy Policy page content.' ), + /* translators: 1: URL to edit Privacy Policy page, 2: URL to preview Privacy Policy page, 3: Accessibility text (do not translate). */ + __( 'Edit or preview%3$s your Privacy Policy page content.' ), esc_url( $edit_href ), - esc_url( $view_href ) + esc_url( $view_href ), + $new_tab_indicator ); } ?> diff --git a/src/wp-admin/options-reading.php b/src/wp-admin/options-reading.php index 31facac7edcca..2b2a7c74ea52f 100644 --- a/src/wp-admin/options-reading.php +++ b/src/wp-admin/options-reading.php @@ -197,9 +197,10 @@Learn more about feeds.' ), - __( 'https://developer.wordpress.org/advanced-administration/wordpress/feeds/' ) + /* translators: 1: Documentation URL. 2: Accessibility text (do not translate). */ + __( 'Your theme determines how content is displayed in browsers. Learn more about feeds%2$s.' ), + esc_url( __( 'https://developer.wordpress.org/advanced-administration/wordpress/feeds/' ) ), + ' ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '' ); ?>
diff --git a/src/wp-admin/options-writing.php b/src/wp-admin/options-writing.php index d40dc0578b315..7e91d1d19062c 100644 --- a/src/wp-admin/options-writing.php +++ b/src/wp-admin/options-writing.php @@ -234,9 +234,10 @@ @@ -248,9 +249,10 @@Update Services because of your site’s visibility settings.' ), - __( 'https://developer.wordpress.org/advanced-administration/wordpress/update-services/' ), + /* translators: 1: Documentation URL. 2: Accessibility text (do not translate). 3: URL to Reading Settings screen. */ + __( 'WordPress is not notifying any Update Services%2$s because of your site’s visibility settings.' ), + esc_url( __( 'https://developer.wordpress.org/advanced-administration/wordpress/update-services/' ) ), + ' ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '', 'options-reading.php' ); ?> diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 42d42b3f8781d..ec601253c3278 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -884,6 +884,9 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'site-icon', '/wp-admin/js/site-icon.js', array( 'jquery' ), false, 1 ); $scripts->set_translations( 'site-icon' ); + $scripts->add( 'settings', "/wp-admin/js/settings$suffix.js", array( 'jquery', 'wp-i18n' ), false, 1 ); + $scripts->set_translations( 'settings' ); + // WordPress no longer uses or bundles Prototype or script.aculo.us. These are now pulled from an external source. $scripts->add( 'prototype', 'https://ajax.googleapis.com/ajax/libs/prototype/1.7.1.0/prototype.js', array(), '1.7.1' ); $scripts->add( 'scriptaculous-root', 'https://ajax.googleapis.com/ajax/libs/scriptaculous/1.9.0/scriptaculous.js', array( 'prototype' ), '1.9.0' ); diff --git a/tests/qunit/index.html b/tests/qunit/index.html index 9fd35f0c1ffc2..c080e981aa938 100644 --- a/tests/qunit/index.html +++ b/tests/qunit/index.html @@ -111,6 +111,7 @@ + + diff --git a/tests/qunit/wp-admin/js/settings.js b/tests/qunit/wp-admin/js/settings.js new file mode 100644 index 0000000000000..a872b989e244b --- /dev/null +++ b/tests/qunit/wp-admin/js/settings.js @@ -0,0 +1,93 @@ +/* global QUnit, wp */ +jQuery( function( $ ) { + QUnit.module( 'wp.admin.settings', { + beforeEach: function() { + // Create the test form. + this.$form = $( '
' ) + .appendTo( '#qunit-fixture' ); + + // Manually initialize settings module behavior for this test form. + // (The production module runs at DOM ready before the test form exists, + // so we replicate its initialization here after the form is created.) + this.originalData = this.$form.serialize(); + this.isSubmitting = false; + var self = this; + + // Define the startWatchingForUnload function. + this.startWatchingForUnload = function() { + self.$form.off( 'change.settings input.settings', self.startWatchingForUnload ); + $( window ).on( 'beforeunload.settings', function() { + if ( ! self.isSubmitting && self.originalData !== self.$form.serialize() ) { + return wp.i18n.__( 'The changes you made will be lost if you navigate away from this page.' ); + } + } ); + }; + + // Attach submit handler to suppress warning on intentional form submission. + this.$form.on( 'submit.settings', function() { + self.isSubmitting = true; + $( window ).off( 'beforeunload.settings' ); + } ); + + // Attach the lazy beforeunload listener on first change. + this.$form.on( 'change.settings input.settings', this.startWatchingForUnload ); + }, + afterEach: function() { + $( window ).off( 'beforeunload.settings' ); + this.$form.remove(); + } + } ); + + QUnit.test( 'No warning when no changes made', function( assert ) { + // beforeunload should not be bound yet (lazy attach). + var result = $( window ).triggerHandler( 'beforeunload.settings' ); + assert.strictEqual( result, undefined, 'No warning shown when form is unchanged.' ); + } ); + + QUnit.test( 'Warning fires when form is dirty', function( assert ) { + // Simulate a field change. + this.$form.find( 'input' ).val( 'changed' ).trigger( 'change' ); + + // Now beforeunload should be attached. + var result = $( window ).triggerHandler( 'beforeunload.settings' ); + assert.ok( result, 'Warning message returned when form has unsaved changes.' ); + assert.ok( result.indexOf( 'changes you made' ) > -1, 'Warning message contains expected text.' ); + } ); + + QUnit.test( 'No warning after form is submitted', function( assert ) { + // Simulate a change. + this.$form.find( 'input' ).val( 'changed' ).trigger( 'change' ); + + // Simulate form submission (saves settings). + this.$form.trigger( 'submit' ); + + // Now beforeunload should not fire or should be removed. + var result = $( window ).triggerHandler( 'beforeunload.settings' ); + assert.strictEqual( result, undefined, 'No warning after intentional form submit.' ); + } ); + + QUnit.test( 'No warning when form is reverted to original', function( assert ) { + // Simulate a change. + this.$form.find( 'input' ).val( 'changed' ).trigger( 'change' ); + + // Revert to original value. + this.$form.find( 'input' ).val( 'original' ).trigger( 'change' ); + + // No warning because serialize matches original. + var result = $( window ).triggerHandler( 'beforeunload.settings' ); + assert.strictEqual( result, undefined, 'No warning when changes are reverted to original state.' ); + } ); + + QUnit.test( 'beforeunload listener is lazy (not attached until first change)', function( assert ) { + // Before any change, beforeunload should return undefined (no handler attached). + var resultBefore = $( window ).triggerHandler( 'beforeunload.settings' ); + assert.strictEqual( resultBefore, undefined, 'beforeunload listener not attached until first change.' ); + + // Now trigger a change on the main test form. + this.$form.find( 'input' ).val( 'changed' ).trigger( 'change' ); + + // After a change, beforeunload should return a message. + var resultAfter = $( window ).triggerHandler( 'beforeunload.settings' ); + assert.ok( resultAfter, 'beforeunload listener attached after first change.' ); + } ); +} );