comment-reply.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /**
  2. * Handles the addition of the comment form.
  3. *
  4. * @since 2.7.0
  5. * @output wp-includes/js/comment-reply.js
  6. *
  7. * @namespace addComment
  8. *
  9. * @type {Object}
  10. */
  11. window.addComment = ( function( window ) {
  12. // Avoid scope lookups on commonly used variables.
  13. var document = window.document;
  14. // Settings.
  15. var config = {
  16. commentReplyClass : 'comment-reply-link',
  17. cancelReplyId : 'cancel-comment-reply-link',
  18. commentFormId : 'commentform',
  19. temporaryFormId : 'wp-temp-form-div',
  20. parentIdFieldId : 'comment_parent',
  21. postIdFieldId : 'comment_post_ID'
  22. };
  23. // Cross browser MutationObserver.
  24. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  25. // Check browser cuts the mustard.
  26. var cutsTheMustard = 'querySelector' in document && 'addEventListener' in window;
  27. /*
  28. * Check browser supports dataset.
  29. * !! sets the variable to true if the property exists.
  30. */
  31. var supportsDataset = !! document.documentElement.dataset;
  32. // For holding the cancel element.
  33. var cancelElement;
  34. // For holding the comment form element.
  35. var commentFormElement;
  36. // The respond element.
  37. var respondElement;
  38. // The mutation observer.
  39. var observer;
  40. if ( cutsTheMustard && document.readyState !== 'loading' ) {
  41. ready();
  42. } else if ( cutsTheMustard ) {
  43. window.addEventListener( 'DOMContentLoaded', ready, false );
  44. }
  45. /**
  46. * Sets up object variables after the DOM is ready.
  47. *
  48. * @since 5.1.1
  49. */
  50. function ready() {
  51. // Initialise the events.
  52. init();
  53. // Set up a MutationObserver to check for comments loaded late.
  54. observeChanges();
  55. }
  56. /**
  57. * Add events to links classed .comment-reply-link.
  58. *
  59. * Searches the context for reply links and adds the JavaScript events
  60. * required to move the comment form. To allow for lazy loading of
  61. * comments this method is exposed as window.commentReply.init().
  62. *
  63. * @since 5.1.0
  64. *
  65. * @memberOf addComment
  66. *
  67. * @param {HTMLElement} context The parent DOM element to search for links.
  68. */
  69. function init( context ) {
  70. if ( ! cutsTheMustard ) {
  71. return;
  72. }
  73. // Get required elements.
  74. cancelElement = getElementById( config.cancelReplyId );
  75. commentFormElement = getElementById( config.commentFormId );
  76. // No cancel element, no replies.
  77. if ( ! cancelElement ) {
  78. return;
  79. }
  80. cancelElement.addEventListener( 'touchstart', cancelEvent );
  81. cancelElement.addEventListener( 'click', cancelEvent );
  82. // Submit the comment form when the user types CTRL or CMD + 'Enter'.
  83. var submitFormHandler = function( e ) {
  84. if ( ( e.metaKey || e.ctrlKey ) && e.keyCode === 13 ) {
  85. commentFormElement.removeEventListener( 'keydown', submitFormHandler );
  86. e.preventDefault();
  87. // The submit button ID is 'submit' so we can't call commentFormElement.submit(). Click it instead.
  88. commentFormElement.submit.click();
  89. return false;
  90. }
  91. };
  92. if ( commentFormElement ) {
  93. commentFormElement.addEventListener( 'keydown', submitFormHandler );
  94. }
  95. var links = replyLinks( context );
  96. var element;
  97. for ( var i = 0, l = links.length; i < l; i++ ) {
  98. element = links[i];
  99. element.addEventListener( 'touchstart', clickEvent );
  100. element.addEventListener( 'click', clickEvent );
  101. }
  102. }
  103. /**
  104. * Return all links classed .comment-reply-link.
  105. *
  106. * @since 5.1.0
  107. *
  108. * @param {HTMLElement} context The parent DOM element to search for links.
  109. *
  110. * @return {HTMLCollection|NodeList|Array}
  111. */
  112. function replyLinks( context ) {
  113. var selectorClass = config.commentReplyClass;
  114. var allReplyLinks;
  115. // childNodes is a handy check to ensure the context is a HTMLElement.
  116. if ( ! context || ! context.childNodes ) {
  117. context = document;
  118. }
  119. if ( document.getElementsByClassName ) {
  120. // Fastest.
  121. allReplyLinks = context.getElementsByClassName( selectorClass );
  122. }
  123. else {
  124. // Fast.
  125. allReplyLinks = context.querySelectorAll( '.' + selectorClass );
  126. }
  127. return allReplyLinks;
  128. }
  129. /**
  130. * Cancel event handler.
  131. *
  132. * @since 5.1.0
  133. *
  134. * @param {Event} event The calling event.
  135. */
  136. function cancelEvent( event ) {
  137. var cancelLink = this;
  138. var temporaryFormId = config.temporaryFormId;
  139. var temporaryElement = getElementById( temporaryFormId );
  140. if ( ! temporaryElement || ! respondElement ) {
  141. // Conditions for cancel link fail.
  142. return;
  143. }
  144. getElementById( config.parentIdFieldId ).value = '0';
  145. // Move the respond form back in place of the temporary element.
  146. temporaryElement.parentNode.replaceChild( respondElement ,temporaryElement );
  147. cancelLink.style.display = 'none';
  148. event.preventDefault();
  149. }
  150. /**
  151. * Click event handler.
  152. *
  153. * @since 5.1.0
  154. *
  155. * @param {Event} event The calling event.
  156. */
  157. function clickEvent( event ) {
  158. var replyLink = this,
  159. commId = getDataAttribute( replyLink, 'belowelement'),
  160. parentId = getDataAttribute( replyLink, 'commentid' ),
  161. respondId = getDataAttribute( replyLink, 'respondelement'),
  162. postId = getDataAttribute( replyLink, 'postid'),
  163. follow;
  164. if ( ! commId || ! parentId || ! respondId || ! postId ) {
  165. /*
  166. * Theme or plugin defines own link via custom `wp_list_comments()` callback
  167. * and calls `moveForm()` either directly or via a custom event hook.
  168. */
  169. return;
  170. }
  171. /*
  172. * Third party comments systems can hook into this function via the global scope,
  173. * therefore the click event needs to reference the global scope.
  174. */
  175. follow = window.addComment.moveForm(commId, parentId, respondId, postId);
  176. if ( false === follow ) {
  177. event.preventDefault();
  178. }
  179. }
  180. /**
  181. * Creates a mutation observer to check for newly inserted comments.
  182. *
  183. * @since 5.1.0
  184. */
  185. function observeChanges() {
  186. if ( ! MutationObserver ) {
  187. return;
  188. }
  189. var observerOptions = {
  190. childList: true,
  191. subtree: true
  192. };
  193. observer = new MutationObserver( handleChanges );
  194. observer.observe( document.body, observerOptions );
  195. }
  196. /**
  197. * Handles DOM changes, calling init() if any new nodes are added.
  198. *
  199. * @since 5.1.0
  200. *
  201. * @param {Array} mutationRecords Array of MutationRecord objects.
  202. */
  203. function handleChanges( mutationRecords ) {
  204. var i = mutationRecords.length;
  205. while ( i-- ) {
  206. // Call init() once if any record in this set adds nodes.
  207. if ( mutationRecords[ i ].addedNodes.length ) {
  208. init();
  209. return;
  210. }
  211. }
  212. }
  213. /**
  214. * Backward compatible getter of data-* attribute.
  215. *
  216. * Uses element.dataset if it exists, otherwise uses getAttribute.
  217. *
  218. * @since 5.1.0
  219. *
  220. * @param {HTMLElement} Element DOM element with the attribute.
  221. * @param {String} Attribute the attribute to get.
  222. *
  223. * @return {String}
  224. */
  225. function getDataAttribute( element, attribute ) {
  226. if ( supportsDataset ) {
  227. return element.dataset[attribute];
  228. }
  229. else {
  230. return element.getAttribute( 'data-' + attribute );
  231. }
  232. }
  233. /**
  234. * Get element by ID.
  235. *
  236. * Local alias for document.getElementById.
  237. *
  238. * @since 5.1.0
  239. *
  240. * @param {HTMLElement} The requested element.
  241. */
  242. function getElementById( elementId ) {
  243. return document.getElementById( elementId );
  244. }
  245. /**
  246. * Moves the reply form from its current position to the reply location.
  247. *
  248. * @since 2.7.0
  249. *
  250. * @memberOf addComment
  251. *
  252. * @param {String} addBelowId HTML ID of element the form follows.
  253. * @param {String} commentId Database ID of comment being replied to.
  254. * @param {String} respondId HTML ID of 'respond' element.
  255. * @param {String} postId Database ID of the post.
  256. */
  257. function moveForm( addBelowId, commentId, respondId, postId ) {
  258. // Get elements based on their IDs.
  259. var addBelowElement = getElementById( addBelowId );
  260. respondElement = getElementById( respondId );
  261. // Get the hidden fields.
  262. var parentIdField = getElementById( config.parentIdFieldId );
  263. var postIdField = getElementById( config.postIdFieldId );
  264. var element, cssHidden, style;
  265. if ( ! addBelowElement || ! respondElement || ! parentIdField ) {
  266. // Missing key elements, fail.
  267. return;
  268. }
  269. addPlaceHolder( respondElement );
  270. // Set the value of the post.
  271. if ( postId && postIdField ) {
  272. postIdField.value = postId;
  273. }
  274. parentIdField.value = commentId;
  275. cancelElement.style.display = '';
  276. addBelowElement.parentNode.insertBefore( respondElement, addBelowElement.nextSibling );
  277. /*
  278. * This is for backward compatibility with third party commenting systems
  279. * hooking into the event using older techniques.
  280. */
  281. cancelElement.onclick = function() {
  282. return false;
  283. };
  284. // Focus on the first field in the comment form.
  285. try {
  286. for ( var i = 0; i < commentFormElement.elements.length; i++ ) {
  287. element = commentFormElement.elements[i];
  288. cssHidden = false;
  289. // Get elements computed style.
  290. if ( 'getComputedStyle' in window ) {
  291. // Modern browsers.
  292. style = window.getComputedStyle( element );
  293. } else if ( document.documentElement.currentStyle ) {
  294. // IE 8.
  295. style = element.currentStyle;
  296. }
  297. /*
  298. * For display none, do the same thing jQuery does. For visibility,
  299. * check the element computed style since browsers are already doing
  300. * the job for us. In fact, the visibility computed style is the actual
  301. * computed value and already takes into account the element ancestors.
  302. */
  303. if ( ( element.offsetWidth <= 0 && element.offsetHeight <= 0 ) || style.visibility === 'hidden' ) {
  304. cssHidden = true;
  305. }
  306. // Skip form elements that are hidden or disabled.
  307. if ( 'hidden' === element.type || element.disabled || cssHidden ) {
  308. continue;
  309. }
  310. element.focus();
  311. // Stop after the first focusable element.
  312. break;
  313. }
  314. }
  315. catch(e) {
  316. }
  317. /*
  318. * false is returned for backward compatibility with third party commenting systems
  319. * hooking into this function.
  320. */
  321. return false;
  322. }
  323. /**
  324. * Add placeholder element.
  325. *
  326. * Places a place holder element above the #respond element for
  327. * the form to be returned to if needs be.
  328. *
  329. * @since 2.7.0
  330. *
  331. * @param {HTMLelement} respondElement the #respond element holding comment form.
  332. */
  333. function addPlaceHolder( respondElement ) {
  334. var temporaryFormId = config.temporaryFormId;
  335. var temporaryElement = getElementById( temporaryFormId );
  336. if ( temporaryElement ) {
  337. // The element already exists, no need to recreate.
  338. return;
  339. }
  340. temporaryElement = document.createElement( 'div' );
  341. temporaryElement.id = temporaryFormId;
  342. temporaryElement.style.display = 'none';
  343. respondElement.parentNode.insertBefore( temporaryElement, respondElement );
  344. }
  345. return {
  346. init: init,
  347. moveForm: moveForm
  348. };
  349. })( window );