You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
12 KiB

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