Skip to content
Open
74 changes: 74 additions & 0 deletions src/js/_enqueues/admin/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,78 @@ jQuery( function($) {
$edittimestamp.show().trigger( 'focus' );
$timestampdiv.slideUp( 'fast' );
});

var $commentparentdiv = $( '#comment-parent-div' ),
$commentparentdisplay = $( '#comment-parent-display' ),
originalcommentparentdisplay = $commentparentdisplay.html(),
$editcommentparent = $commentparentdiv.siblings( 'a.edit-comment-parent' );

/**
* Adds event that opens the parent comment form if the form is hidden.
*
* @since 7.1.0
*
* @listens $editcommentparent:click
*
* @param {Event} event The event object.
* @return {void}
*/
$editcommentparent.on( 'click', function( event ) {
if ( $commentparentdiv.is( ':hidden' ) ) {
// Slide down the form and set focus on the parent field.
$commentparentdiv.slideDown( 'fast', function() {
$( '#comment_parent' ).trigger( 'focus' );
} );
$(this).hide();
}
event.preventDefault();
});

/**
* Resets the parent comment value when the cancel button is clicked.
*
* @since 7.1.0
*
* @listens .cancel-comment-parent:click
*
* @param {Event} event The event object.
* @return {void}
*/
$commentparentdiv.find( '.cancel-comment-parent' ).on( 'click', function( event ) {
// Move focus back to the Edit link.
$editcommentparent.show().trigger( 'focus' );
$commentparentdiv.slideUp( 'fast' );
$( '#comment_parent' ).val( $( '#hidden_comment_parent' ).val() );
$commentparentdisplay.html( originalcommentparentdisplay );
event.preventDefault();
});

/**
* Updates the parent comment display when the ok button is clicked.
*
* @since 7.1.0
*
* @listens .save-comment-parent:click
*
* @param {Event} event The event object.
* @return {void}
*/
$commentparentdiv.find( '.save-comment-parent' ).on( 'click', function( event ) {
var $selected = $( '#comment_parent option:selected' ),
parentLabel = $selected.data( 'author' ) || $selected.text();

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.

Suggested change
parentLabel = $selected.data( 'author' ) || $selected.text();
parentLabel = '0' === $selected.val() ?
/* translators: Displayed when a comment has no parent. */
wp.i18n.__( 'None' ) :
$selected.data( 'author' ) || $selected.text();

Otherwise, this can show:

Image

But upon reloading:

Image


event.preventDefault();

$commentparentdisplay.html(
wp.i18n.sprintf(
/* translators: %s: Parent comment author. */

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.

Aligning the translator comments for the same strings.

Suggested change
/* translators: %s: Parent comment author. */
/* translators: %s: Parent comment link, or 'None'. */

wp.i18n.__( 'In reply to: %s' ),
$( '<b />' ).text( parentLabel ).prop( 'outerHTML' )
)
);

// Move focus back to the Edit link.
$editcommentparent.show().trigger( 'focus' );
$commentparentdiv.slideUp( 'fast' );
});
});
10 changes: 10 additions & 0 deletions src/wp-admin/css/edit.css
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,16 @@ form#tags-filter {
line-height: 1.76923076;
}

#comment-parent-div {
padding-top: 5px;
/* Fieldsets cannot shrink below their content by default, allow it so the select cannot cause overflow. */
min-width: 0;
}

#comment-parent-div select {
width: 100%;
}

#timestampdiv p {
margin: 8px 0 6px;
}
Expand Down
188 changes: 173 additions & 15 deletions src/wp-admin/edit-form-comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,25 +201,183 @@
</div>

<?php
if ( $comment->comment_parent ) :
$parent_display = __( 'None' );

if ( $comment->comment_parent ) {
$parent = get_comment( $comment->comment_parent );
if ( $parent ) :
$parent_link = esc_url( get_comment_link( $parent ) );
$name = get_comment_author( $parent );
?>
<div class="misc-pub-section misc-pub-reply-to">

if ( $parent ) {
$parent_display = sprintf(
'<a href="%s">%s</a>',
esc_url( get_comment_link( $parent ) ),
esc_html( get_comment_author( $parent ) )
);
}
}

// The parent can only be changed when threaded comments are enabled.
$comment_threading_enabled = get_option( 'thread_comments' );

if ( $comment_threading_enabled ) {
Comment thread
youknowriad marked this conversation as resolved.
$max_thread_depth = (int) get_option( 'thread_comments_depth' );

// Limit the number of comments to keep memory usage and the size of the dropdown reasonable on busy posts.
$post_comments = get_comments(
array(
'post_id' => $comment->comment_post_ID,
'type' => 'comment',
'status' => array( 'approve', 'hold' ),
'orderby' => 'comment_date_gmt',
'order' => 'DESC',
'number' => 100,
)
);

// Restore chronological order for display.
$post_comments = array_reverse( $post_comments );

// Index the comments by ID and by parent, to compute depths and find descendants.
$post_comments_by_id = array();
$comments_by_parent = array();

foreach ( $post_comments as $post_comment ) {
$post_comments_by_id[ (int) $post_comment->comment_ID ] = $post_comment;
$comments_by_parent[ (int) $post_comment->comment_parent ][] = (int) $post_comment->comment_ID;
}

// Walk the descendants level by level, which also gives the height of the subtree that moves with the comment.
$comment_descendants = array();
$comment_subtree_height = 1;
$comment_level = array( (int) $comment->comment_ID );

while ( $comment_level ) {
$next_level = array();

foreach ( $comment_level as $level_comment_id ) {
if ( isset( $comments_by_parent[ $level_comment_id ] ) ) {
foreach ( $comments_by_parent[ $level_comment_id ] as $descendant_id ) {
$comment_descendants[ $descendant_id ] = true;
$next_level[] = $descendant_id;
}
}
}

if ( $next_level ) {
++$comment_subtree_height;
}

$comment_level = $next_level;
}

// Compute each comment's depth, since comments at the maximum threading depth cannot become a parent.
$comment_depths = array();

foreach ( $post_comments as $post_comment ) {
$depth = 1;
$ancestor_id = (int) $post_comment->comment_parent;

while ( $ancestor_id && isset( $post_comments_by_id[ $ancestor_id ] ) ) {
++$depth;
$ancestor_id = (int) $post_comments_by_id[ $ancestor_id ]->comment_parent;
}

$comment_depths[ (int) $post_comment->comment_ID ] = $depth;
}
}
?>
<div class="misc-pub-section misc-pub-reply-to">
<span id="comment-parent-display">
Comment thread
youknowriad marked this conversation as resolved.
<?php
printf(
/* translators: %s: Parent comment link, or 'None'. */
__( 'In reply to: %s' ),
'<b>' . $parent_display . '</b>'
);
?>
</span>
<?php if ( $comment_threading_enabled ) : ?>
<a href="#edit_comment_parent" class="edit-comment-parent hide-if-no-js"><span aria-hidden="true"><?php _e( 'Edit' ); ?></span> <span class="screen-reader-text">
<?php
/* translators: Hidden accessibility text. */
_e( 'Edit parent comment' );
?>
</span></a>
<fieldset id="comment-parent-div" class="hide-if-js">
<legend class="screen-reader-text">
<?php
/* translators: Hidden accessibility text. */
_e( 'Parent comment' );
?>
</legend>
<label for="comment_parent"><?php _e( 'Parent comment' ); ?></label>
<select name="comment_parent" id="comment_parent">
<option value="0"<?php selected( 0, (int) $comment->comment_parent ); ?>>
<?php
/* translators: Option in the parent comment dropdown, meaning the comment has no parent. */
_e( 'None (top-level comment)' );
?>
</option>
<?php
$current_parent_listed = false;

foreach ( $post_comments as $post_comment ) {
$post_comment_id = (int) $post_comment->comment_ID;

if ( $post_comment_id === (int) $comment->comment_ID || isset( $comment_descendants[ $post_comment_id ] ) ) {
continue;
}

// The comment and the replies that move with it must stay within the maximum threading depth.
if ( $max_thread_depth && $comment_depths[ $post_comment_id ] + $comment_subtree_height > $max_thread_depth ) {
continue;
}

if ( $post_comment_id === (int) $comment->comment_parent ) {
$current_parent_listed = true;
}

$option_label = sprintf(
/* translators: 1: Comment author, 2: Comment excerpt. */
__( '%1$s: %2$s' ),
get_comment_author( $post_comment ),
wp_html_excerpt( get_comment_excerpt( $post_comment ), 50, '…' )
);

printf(
/* translators: %s: Comment link. */
__( 'In reply to: %s' ),
'<b><a href="' . $parent_link . '">' . $name . '</a></b>'
"\t<option value='%d' data-author='%s'%s>%s</option>\n",
$post_comment_id,
esc_attr( get_comment_author( $post_comment ) ),
selected( $post_comment_id, (int) $comment->comment_parent, false ),
esc_html( $option_label )
);
?>
</div>
<?php
endif;
endif;
?>
}

// The current parent may not be listed, e.g. a pingback or a comment no longer publicly visible.
if ( $comment->comment_parent && ! $current_parent_listed && isset( $parent ) ) {
$option_label = sprintf(
/* translators: 1: Comment author, 2: Comment excerpt. */
__( '%1$s: %2$s' ),
get_comment_author( $parent ),
wp_html_excerpt( get_comment_excerpt( $parent ), 50, '…' )
);

printf(
"\t<option value='%d' data-author='%s' selected>%s</option>\n",
(int) $comment->comment_parent,
esc_attr( get_comment_author( $parent ) ),
esc_html( $option_label )
);
}
?>
</select>
<input type="hidden" id="hidden_comment_parent" value="<?php echo esc_attr( $comment->comment_parent ); ?>" />
<p>
<a href="#edit_comment_parent" class="save-comment-parent hide-if-no-js button"><?php _e( 'OK' ); ?></a>
<a href="#edit_comment_parent" class="cancel-comment-parent hide-if-no-js button-cancel"><?php _e( 'Cancel' ); ?></a>
</p>
</fieldset>
<?php endif; ?>
</div>

<?php
/**
Expand Down
80 changes: 80 additions & 0 deletions src/wp-admin/includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function comment_exists( $comment_author, $comment_date, $timezone = 'blog' ) {
*
* @since 2.0.0
* @since 5.5.0 A return value was added.
* @since 7.1.0 The comment parent can be updated via `$_POST['comment_parent']`.
*
* @return int|WP_Error The value 1 if the comment was updated, 0 if not updated.
* A WP_Error object on failure.
Expand Down Expand Up @@ -74,6 +75,85 @@ function edit_comment() {
$_POST['comment_ID'] = (int) $_POST['comment_ID'];
}

if ( isset( $_POST['comment_parent'] ) ) {
$comment_id = (int) $_POST['comment_ID'];
$comment_parent = (int) $_POST['comment_parent'];

$_POST['comment_parent'] = $comment_parent;

$comment = get_comment( $comment_id );

if ( $comment && $comment_parent && $comment_parent !== (int) $comment->comment_parent ) {
if ( ! get_option( 'thread_comments' ) ) {
return new WP_Error( 'comment_parent_invalid', __( 'The comment parent cannot be changed because threaded comments are disabled.' ) );
}

if ( $comment_parent === $comment_id ) {
return new WP_Error( 'comment_parent_invalid', __( 'A comment cannot be a reply to itself.' ) );
}
Comment thread
youknowriad marked this conversation as resolved.

$parent = get_comment( $comment_parent );

// The parent must be a comment of the same type, on the same post, and not in the Trash or marked as spam.
if (
! $parent
|| (int) $parent->comment_post_ID !== (int) $comment->comment_post_ID
|| $parent->comment_type !== $comment->comment_type
|| in_array( wp_get_comment_status( $parent ), array( 'spam', 'trash' ), true )
) {
return new WP_Error( 'comment_parent_invalid', __( 'Invalid parent comment.' ) );
}

// Walk up the new parent's ancestors to prevent creating a threading loop.
$ancestors = array();
$ancestor = $parent;
$parent_depth = 1;

while ( $ancestor && $ancestor->comment_parent && ! isset( $ancestors[ $ancestor->comment_ID ] ) ) {
if ( (int) $ancestor->comment_parent === $comment_id ) {
return new WP_Error( 'comment_parent_invalid', __( 'A comment cannot be a reply to one of its own replies.' ) );
}

$ancestors[ $ancestor->comment_ID ] = true;
++$parent_depth;

$ancestor = get_comment( $ancestor->comment_parent );
}

$max_thread_depth = (int) get_option( 'thread_comments_depth' );

if ( $max_thread_depth ) {
/*
* The comment's replies move with it, so the whole subtree must stay within
* the maximum depth. Measure its height one level of replies at a time,
* stopping as soon as the subtree cannot fit, which also bounds the loop
* should the stored comment hierarchy contain a cycle.
*/
$subtree_height = 1;
$level_ids = array( $comment_id );

while ( $level_ids && $parent_depth + $subtree_height <= $max_thread_depth ) {
$level_ids = get_comments(
array(
'parent__in' => $level_ids,
'fields' => 'ids',
'status' => 'any',
'orderby' => 'none',
)
);

if ( $level_ids ) {
++$subtree_height;
}
}

if ( $parent_depth + $subtree_height > $max_thread_depth ) {
return new WP_Error( 'comment_parent_invalid', __( 'The comment cannot be moved there because it or its replies would exceed the maximum threading depth.' ) );
}
}
Comment thread
youknowriad marked this conversation as resolved.
}
}

foreach ( array( 'aa', 'mm', 'jj', 'hh', 'mn' ) as $timeunit ) {
if ( ! empty( $_POST[ 'hidden_' . $timeunit ] ) && $_POST[ 'hidden_' . $timeunit ] !== $_POST[ $timeunit ] ) {
$_POST['edit_date'] = '1';
Expand Down
Loading
Loading