From f617a2d6d932e658449cef1b7864fb77080ecd09 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 3 Dec 2017 22:37:45 +0000 Subject: [PATCH] Comments: Modernise JavaScript for comment reply links. Update the comment reply JavaScript to be unobtrusive and use events rather than inline `onclick` attributes. Along with bringing the code into the 2010s this prevents an edge-case in which `addComment.moveForm()` could be called before the JavaScript has loaded. Props peterwilsoncc, bradparbs. Fixes #31590. Built from https://develop.svn.wordpress.org/trunk@42360 git-svn-id: http://core.svn.wordpress.org/trunk@42189 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/comment-template.php | 22 +- wp-includes/js/comment-reply.js | 326 ++++++++++++++++++++++------ wp-includes/js/comment-reply.min.js | 2 +- wp-includes/version.php | 2 +- 4 files changed, 276 insertions(+), 76 deletions(-) diff --git a/wp-includes/comment-template.php b/wp-includes/comment-template.php index 0f8f7a39c8..3b7a46c8cb 100644 --- a/wp-includes/comment-template.php +++ b/wp-includes/comment-template.php @@ -1667,15 +1667,25 @@ function get_comment_reply_link( $args = array(), $comment = null, $post = null $args['login_text'] ); } else { - $onclick = sprintf( - 'return addComment.moveForm( "%1$s-%2$s", "%2$s", "%3$s", "%4$s" )', - $args['add_below'], $comment->comment_ID, $args['respond_id'], $post->ID + $data_attributes = array( + 'commentid' => $comment->comment_ID, + 'postid' => $post->ID, + 'belowelement' => $args['add_below'] . '-' . $comment->comment_ID, + 'respondelement' => $args['respond_id'], ); + $data_attribute_string = ''; + + foreach ( $data_attributes as $name => $value ) { + $data_attribute_string .= " data-${name}=\"" . esc_attr( $value ) . "\""; + } + + $data_attribute_string = trim( $data_attribute_string ); + $link = sprintf( - "%s", - esc_url( add_query_arg( 'replytocom', $comment->comment_ID, get_permalink( $post->ID ) ) ) . '#' . $args['respond_id'], - $onclick, + "%s", + esc_url( add_query_arg( 'replytocom', $comment->comment_ID ) ) . "#" . $args['respond_id'], + $data_attribute_string, esc_attr( sprintf( $args['reply_to_text'], $comment->comment_author ) ), $args['reply_text'] ); diff --git a/wp-includes/js/comment-reply.js b/wp-includes/js/comment-reply.js index 3ddf7744ce..55a4e9c9b0 100644 --- a/wp-includes/js/comment-reply.js +++ b/wp-includes/js/comment-reply.js @@ -5,86 +5,252 @@ * * @type {Object} */ -var addComment = { - /** - * @summary Retrieves the elements corresponding to the given IDs. - * - * @since 2.7.0 - * - * @param {string} commId The comment ID. - * @param {string} parentId The parent ID. - * @param {string} respondId The respond ID. - * @param {string} postId The post ID. - * @returns {boolean} Always returns false. - */ - moveForm: function( commId, parentId, respondId, postId ) { - var div, element, style, cssHidden, - t = this, - comm = t.I( commId ), - respond = t.I( respondId ), - cancel = t.I( 'cancel-comment-reply-link' ), - parent = t.I( 'comment_parent' ), - post = t.I( 'comment_post_ID' ), - commentForm = respond.getElementsByTagName( 'form' )[0]; +var addComment; +addComment = ( function( window ) { + // Avoid scope lookups on commonly used variables. + var document = window.document; - if ( ! comm || ! respond || ! cancel || ! parent || ! commentForm ) { + // Settings. + var config = { + commentReplyClass : 'comment-reply-link', + cancelReplyId : 'cancel-comment-reply-link', + commentFormId : 'commentform', + temporaryFormId : 'wp-temp-form-div', + parentIdFieldId : 'comment_parent', + postIdFieldId : 'comment_post_ID' + }; + + // Check browser cuts the mustard. + var cutsTheMustard = 'querySelector' in document && 'addEventListener' in window; + + /* + * Check browser supports dataset. + * !! sets the variable to true if the property exists. + */ + var supportsDataset = !! document.body.dataset; + + // For holding the cancel element. + var cancelElement; + + // For holding the comment form element. + var commentFormElement; + + // The respond element. + var respondElement; + + // Initialise the events. + init(); + + /** + * Add events to links classed .comment-reply-link. + * + * Searches the context for reply links and adds the JavaScript events + * required to move the comment form. To allow for lazy loading of + * comments this method is exposed as window.commentReply.init(). + * + * @since 5.0.0 + * + * @param {HTMLElement} context The parent DOM element to search for links. + */ + function init( context ) { + if ( true !== cutsTheMustard ) { return; } - t.respondId = respondId; - postId = postId || false; + // Get required elements. + cancelElement = getElementById( config.cancelReplyId ); + commentFormElement = getElementById( config.commentFormId ); - if ( ! t.I( 'wp-temp-form-div' ) ) { - div = document.createElement( 'div' ); - div.id = 'wp-temp-form-div'; - div.style.display = 'none'; - respond.parentNode.insertBefore( div, respond ); + // No cancel element, no replies. + if ( ! cancelElement ) { + return; } - comm.parentNode.insertBefore( respond, comm.nextSibling ); - if ( post && postId ) { - post.value = postId; - } - parent.value = parentId; - cancel.style.display = ''; + cancelElement.addEventListener( 'touchstart', cancelEvent ); + cancelElement.addEventListener( 'click', cancelEvent ); - /** - * @summary Puts back the comment, hides the cancel button and removes the onclick event. - * - * @returns {boolean} Always returns false. + var links = replyLinks( context ); + var element; + + for ( var i = 0, l = links.length; i < l; i++ ) { + element = links[i]; + + element.addEventListener( 'touchstart', clickEvent ); + element.addEventListener( 'click', clickEvent ); + } + } + + /** + * Return all links classed .comment-reply-link. + * + * @since 5.0.0 + * + * @param {HTMLElement} context The parent DOM element to search for links. + * + * @return {HTMLCollection|NodeList|Array} + */ + function replyLinks( context ) { + var selectorClass = config.commentReplyClass; + var allReplyLinks; + + // childNodes is a handy check to ensure the context is a HTMLElement. + if ( ! context || ! context.childNodes ) { + context = document; + } + + if ( document.getElementsByClassName ) { + // Fastest. + allReplyLinks = context.getElementsByClassName( selectorClass ); + } + else { + // Fast. + allReplyLinks = context.querySelectorAll( '.' + selectorClass ); + } + + return allReplyLinks; + } + + /** + * Cancel event handler. + * + * @since 5.0.0 + * + * @param {Event} event The calling event. + */ + function cancelEvent( event ) { + var cancelLink = this; + var temporaryFormId = config.temporaryFormId; + var temporaryElement = getElementById( temporaryFormId ); + + if ( ! temporaryElement || ! respondElement ) { + // Conditions for cancel link fail. + return; + } + + getElementById( config.parentIdFieldId ).value = '0'; + + // Move the respond form back in place of the temporary element. + temporaryElement.parentNode.replaceChild( respondElement ,temporaryElement ); + cancelLink.style.display = 'none'; + event.preventDefault(); + } + + /** + * Click event handler. + * + * @since 5.0.0 + * + * @param {Event} event The calling event. + */ + function clickEvent( event ) { + var replyLink = this, + commId = getDataAttribute( replyLink, 'belowelement'), + parentId = getDataAttribute( replyLink, 'commentid' ), + respondId = getDataAttribute( replyLink, 'respondelement'), + postId = getDataAttribute( replyLink, 'postid'), + follow; + + /* + * Third party comments systems can hook into this function via the global scope, + * therefore the click event needs to reference the global scope. */ - cancel.onclick = function() { - var t = addComment, - temp = t.I( 'wp-temp-form-div' ), - respond = t.I( t.respondId ); + follow = window.addComment.moveForm(commId, parentId, respondId, postId); + if ( false === follow ) { + event.preventDefault(); + } + } - if ( ! temp || ! respond ) { - return; - } + /** + * Backward compatible getter of data-* attribute. + * + * Uses element.dataset if it exists, otherwise uses getAttribute. + * + * @since 5.0.0 + * + * @param {HTMLElement} Element DOM element with the attribute. + * @param {String} Attribute the attribute to get. + * + * @return {String} + */ + function getDataAttribute( element, attribute ) { + if ( supportsDataset ) { + return element.dataset[attribute]; + } + else { + return element.getAttribute( 'data-' + attribute ); + } + } - t.I( 'comment_parent' ).value = '0'; - temp.parentNode.insertBefore( respond, temp ); - temp.parentNode.removeChild( temp ); - this.style.display = 'none'; - this.onclick = null; + /** + * Get element by ID. + * + * Local alias for document.getElementById. + * + * @since 5.0.0 + * + * @param {HTMLElement} The requested element. + */ + function getElementById( elementId ) { + return document.getElementById( elementId ); + } + + /** + * Moves the reply form from its current position to the reply location. + * + * @since 2.7.0 + * + * @param {String} addBelowId HTML ID of element the form follows. + * @param {String} commentId Database ID of comment being replied to. + * @param {String} respondId HTML ID of 'respond' element. + * @param {String} postId Database ID of the post. + */ + function moveForm( addBelowId, commentId, respondId, postId ) { + // Get elements based on their IDs. + var addBelowElement = getElementById( addBelowId ); + respondElement = getElementById( respondId ); + + // Get the hidden fields. + var parentIdField = getElementById( config.parentIdFieldId ); + var postIdField = getElementById( config.postIdFieldId ); + var element, cssHidden, style; + + if ( ! addBelowElement || ! respondElement || ! parentIdField ) { + // Missing key elements, fail. + return; + } + + addPlaceHolder( respondElement ); + + // Set the value of the post. + if ( postId && postIdField ) { + postIdField.value = postId; + } + + parentIdField.value = commentId; + + cancelElement.style.display = ''; + addBelowElement.parentNode.insertBefore( respondElement, addBelowElement.nextSibling ); + + /* + * This is for backward compatibility with third party commenting systems + * hooking into the event using older techniques. + */ + cancelElement.onclick = function(){ return false; }; - /* - * Sets initial focus to the first form focusable element. - * Uses try/catch just to avoid errors in IE 7- which return visibility - * 'inherit' when the visibility value is inherited from an ancestor. - */ + // Focus on the first field in the comment form. try { - for ( var i = 0; i < commentForm.elements.length; i++ ) { - element = commentForm.elements[i]; + for ( var i = 0; i < commentFormElement.elements.length; i++ ) { + element = commentFormElement.elements[i]; cssHidden = false; - // Modern browsers. + // Get elements computed style. if ( 'getComputedStyle' in window ) { + // Modern browsers. style = window.getComputedStyle( element ); - // IE 8. } else if ( document.documentElement.currentStyle ) { + // IE 8. style = element.currentStyle; } @@ -107,21 +273,45 @@ var addComment = { // Stop after the first focusable element. break; } + } + catch(e) { - } catch( er ) {} + } + /* + * false is returned for backward compatibility with third party commenting systems + * hooking into this function. + */ return false; - }, + } /** - * @summary Returns the object corresponding to the given ID. + * Add placeholder element. + * + * Places a place holder element above the #respond element for + * the form to be returned to if needs be. * * @since 2.7.0 * - * @param {string} id The ID. - * @returns {Element} The element belonging to the ID. + * @param {HTMLelement} respondElement the #respond element holding comment form. */ - I: function( id ) { - return document.getElementById( id ); + function addPlaceHolder( respondElement ) { + var temporaryFormId = config.temporaryFormId; + var temporaryElement = getElementById( temporaryFormId ); + + if ( temporaryElement ) { + // The element already exists, no need to recreate. + return; + } + + temporaryElement = document.createElement( 'div' ); + temporaryElement.id = temporaryFormId; + temporaryElement.style.display = 'none'; + respondElement.parentNode.insertBefore( temporaryElement, respondElement ); } -}; + + return { + init: init, + moveForm: moveForm + }; +})( window ); diff --git a/wp-includes/js/comment-reply.min.js b/wp-includes/js/comment-reply.min.js index 4042143700..26a2c33085 100644 --- a/wp-includes/js/comment-reply.min.js +++ b/wp-includes/js/comment-reply.min.js @@ -1 +1 @@ -var addComment={moveForm:function(a,b,c,d){var e,f,g,h,i=this,j=i.I(a),k=i.I(c),l=i.I("cancel-comment-reply-link"),m=i.I("comment_parent"),n=i.I("comment_post_ID"),o=k.getElementsByTagName("form")[0];if(j&&k&&l&&m&&o){i.respondId=c,d=d||!1,i.I("wp-temp-form-div")||(e=document.createElement("div"),e.id="wp-temp-form-div",e.style.display="none",k.parentNode.insertBefore(e,k)),j.parentNode.insertBefore(k,j.nextSibling),n&&d&&(n.value=d),m.value=b,l.style.display="",l.onclick=function(){var a=addComment,b=a.I("wp-temp-form-div"),c=a.I(a.respondId);if(b&&c)return a.I("comment_parent").value="0",b.parentNode.insertBefore(c,b),b.parentNode.removeChild(b),this.style.display="none",this.onclick=null,!1};try{for(var p=0;p