Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ],
Expand Down Expand Up @@ -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',
Expand Down
50 changes: 50 additions & 0 deletions src/js/_enqueues/admin/settings.js
Original file line number Diff line number Diff line change
@@ -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 );
17 changes: 12 additions & 5 deletions src/wp-admin/options-general.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,10 @@ class="<?php echo esc_attr( $classes_for_button ); ?>"
<p class="description" id="home-description">
<?php
printf(
/* translators: %s: Documentation URL. */
__( 'Enter the same address here unless you <a href="%s">want your site home page to be different from your WordPress installation directory</a>.' ),
__( '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 <a href="%1$s" target="_blank" rel="noopener noreferrer">want your site home page to be different from your WordPress installation directory%2$s</a>.' ),
esc_url( __( 'https://developer.wordpress.org/advanced-administration/server/wordpress-in-directory/' ) ),
'<span class="screen-reader-text"> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '</span><span aria-hidden="true" class="dashicons dashicons-external"></span>'
);
?>
</p>
Expand Down Expand Up @@ -568,8 +569,14 @@ class="<?php echo esc_attr( $classes_for_button ); ?>"
'<p><strong>' . __( 'Preview:' ) . '</strong> <span class="example">' . date_i18n( get_option( 'time_format' ) ) . '</span>' .
"<span class='spinner'></span>\n" . '</p>';

echo "\t<p class='date-time-doc'>" . __( '<a href="https://wordpress.org/documentation/article/customize-date-and-time-format/">Documentation on date and time formatting</a>.' ) . "</p>\n";
?>
printf(
"\t<p class='date-time-doc'><a href=\"%1\$s\" target=\"_blank\" rel=\"noopener noreferrer\">%2\$s<span class=\"screen-reader-text\"> %3\$s</span><span aria-hidden=\"true\" class=\"dashicons dashicons-external\"></span></a>.</p>\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)' )
);
?>
</fieldset>
</td>
</tr>
Expand Down
2 changes: 2 additions & 0 deletions src/wp-admin/options-head.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@
}

settings_errors();

wp_enqueue_script( 'settings' );
7 changes: 4 additions & 3 deletions src/wp-admin/options-permalink.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,10 @@
<p>
<?php
printf(
/* translators: %s: Documentation URL. */
__( '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 <a href="%s">number of tags are available</a>, 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 <a href="%1$s" target="_blank" rel="noopener noreferrer">number of tags are available%2$s</a>, and here are some examples to get you started.' ),
esc_url( __( 'https://wordpress.org/documentation/article/customize-permalinks/' ) ),
'<span class="screen-reader-text"> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '</span><span aria-hidden="true" class="dashicons dashicons-external"></span>'
);
?>
</p>
Expand Down
16 changes: 10 additions & 6 deletions src/wp-admin/options-privacy.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,19 +215,23 @@ static function ( $body_class ) {
?>
<strong>
<?php
$new_tab_indicator = '<span class="screen-reader-text"> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '</span><span aria-hidden="true" class="dashicons dashicons-external"></span>';

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. */
__( '<a href="%1$s">Edit</a> or <a href="%2$s">view</a> 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). */
__( '<a href="%1$s">Edit</a> or <a href="%2$s" target="_blank" rel="noopener noreferrer">view%3$s</a> 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. */
__( '<a href="%1$s">Edit</a> or <a href="%2$s">preview</a> 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). */
__( '<a href="%1$s">Edit</a> or <a href="%2$s" target="_blank" rel="noopener noreferrer">preview%3$s</a> your Privacy Policy page content.' ),
esc_url( $edit_href ),
esc_url( $view_href )
esc_url( $view_href ),
$new_tab_indicator
);
}
?>
Expand Down
7 changes: 4 additions & 3 deletions src/wp-admin/options-reading.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,10 @@
<p class="description">
<?php
printf(
/* translators: %s: Documentation URL. */
__( 'Your theme determines how content is displayed in browsers. <a href="%s">Learn more about feeds</a>.' ),
__( '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. <a href="%1$s" target="_blank" rel="noopener noreferrer">Learn more about feeds%2$s</a>.' ),
esc_url( __( 'https://developer.wordpress.org/advanced-administration/wordpress/feeds/' ) ),
'<span class="screen-reader-text"> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '</span><span aria-hidden="true" class="dashicons dashicons-external"></span>'
);
?>
</p>
Expand Down
14 changes: 8 additions & 6 deletions src/wp-admin/options-writing.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,10 @@
<p><label for="ping_sites">
<?php
printf(
/* translators: %s: Documentation URL. */
__( 'When you publish a new post, WordPress automatically notifies the following site update services. For more about this, see the <a href="%s">Update Services</a> documentation article. Separate multiple service URLs with line breaks.' ),
__( 'https://developer.wordpress.org/advanced-administration/wordpress/update-services/' )
/* translators: 1: Documentation URL. 2: Accessibility text (do not translate). */
__( 'When you publish a new post, WordPress automatically notifies the following site update services. For more about this, see the <a href="%1$s" target="_blank" rel="noopener noreferrer">Update Services%2$s</a> documentation article. Separate multiple service URLs with line breaks.' ),
esc_url( __( 'https://developer.wordpress.org/advanced-administration/wordpress/update-services/' ) ),
'<span class="screen-reader-text"> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '</span><span aria-hidden="true" class="dashicons dashicons-external"></span>'
);
?>
</label></p>
Expand All @@ -248,9 +249,10 @@
<p>
<?php
printf(
/* translators: 1: Documentation URL, 2: URL to Reading Settings screen. */
__( 'WordPress is not notifying any <a href="%1$s">Update Services</a> because of your site&#8217;s <a href="%2$s">visibility settings</a>.' ),
__( '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 <a href="%1$s" target="_blank" rel="noopener noreferrer">Update Services%2$s</a> because of your site&#8217;s <a href="%3$s">visibility settings</a>.' ),
esc_url( __( 'https://developer.wordpress.org/advanced-administration/wordpress/update-services/' ) ),
'<span class="screen-reader-text"> ' . /* translators: Hidden accessibility text. */ __( '(opens in a new tab)' ) . '</span><span aria-hidden="true" class="dashicons dashicons-external"></span>',
'options-reading.php'
);
?>
Expand Down
3 changes: 3 additions & 0 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down
2 changes: 2 additions & 0 deletions tests/qunit/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
<script src="../../build/wp-admin/js/customize-nav-menus.js"></script>
<script src="../../build/wp-admin/js/customize-widgets.js"></script>
<script src="../../build/wp-admin/js/word-count.js"></script>
<script src="../../build/wp-admin/js/settings.js"></script>

<script src="../../build/wp-admin/js/widgets/media-widgets.js"></script>
<script>
Expand Down Expand Up @@ -162,6 +163,7 @@
<script src="wp-admin/js/customize-widgets.js"></script>
<script src="wp-admin/js/word-count.js"></script>
<script src="wp-admin/js/nav-menu.js"></script>
<script src="wp-admin/js/settings.js"></script>
<script src="wp-admin/js/widgets/test-media-widgets.js"></script>
<script src="wp-admin/js/widgets/test-media-image-widget.js"></script>
<script src="wp-admin/js/widgets/test-media-gallery-widget.js"></script>
Expand Down
93 changes: 93 additions & 0 deletions tests/qunit/wp-admin/js/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* global QUnit, wp */
jQuery( function( $ ) {
QUnit.module( 'wp.admin.settings', {
beforeEach: function() {
// Create the test form.
this.$form = $( '<form action="options.php"><input name="foo" value="original"></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.' );
} );
} );
Loading