inline-edit-post.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. /**
  2. * This file contains the functions needed for the inline editing of posts.
  3. *
  4. * @since 2.7.0
  5. * @output wp-admin/js/inline-edit-post.js
  6. */
  7. /* global inlineEditL10n, ajaxurl, typenow, inlineEditPost */
  8. window.wp = window.wp || {};
  9. /**
  10. * Manages the quick edit and bulk edit windows for editing posts or pages.
  11. *
  12. * @namespace inlineEditPost
  13. *
  14. * @since 2.7.0
  15. *
  16. * @type {Object}
  17. *
  18. * @property {string} type The type of inline editor.
  19. * @property {string} what The prefix before the post id.
  20. *
  21. */
  22. ( function( $, wp ) {
  23. window.inlineEditPost = {
  24. /**
  25. * Initializes the inline and bulk post editor.
  26. *
  27. * Binds event handlers to the escape key to close the inline editor
  28. * and to the save and close buttons. Changes DOM to be ready for inline
  29. * editing. Adds event handler to bulk edit.
  30. *
  31. * @memberof inlineEditPost
  32. * @since 2.7.0
  33. *
  34. * @returns {void}
  35. */
  36. init : function(){
  37. var t = this, qeRow = $('#inline-edit'), bulkRow = $('#bulk-edit');
  38. t.type = $('table.widefat').hasClass('pages') ? 'page' : 'post';
  39. // Post id prefix.
  40. t.what = '#post-';
  41. /**
  42. * Binds the escape key to revert the changes and close the quick editor.
  43. *
  44. * @returns {boolean} The result of revert.
  45. */
  46. qeRow.keyup(function(e){
  47. // Revert changes if escape key is pressed.
  48. if ( e.which === 27 ) {
  49. return inlineEditPost.revert();
  50. }
  51. });
  52. /**
  53. * Binds the escape key to revert the changes and close the bulk editor.
  54. *
  55. * @returns {boolean} The result of revert.
  56. */
  57. bulkRow.keyup(function(e){
  58. // Revert changes if escape key is pressed.
  59. if ( e.which === 27 ) {
  60. return inlineEditPost.revert();
  61. }
  62. });
  63. /**
  64. * Reverts changes and close the quick editor if the cancel button is clicked.
  65. *
  66. * @returns {boolean} The result of revert.
  67. */
  68. $( '.cancel', qeRow ).click( function() {
  69. return inlineEditPost.revert();
  70. });
  71. /**
  72. * Saves changes in the quick editor if the save(named: update) button is clicked.
  73. *
  74. * @returns {boolean} The result of save.
  75. */
  76. $( '.save', qeRow ).click( function() {
  77. return inlineEditPost.save(this);
  78. });
  79. /**
  80. * If enter is pressed, and the target is not the cancel button, save the post.
  81. *
  82. * @returns {boolean} The result of save.
  83. */
  84. $('td', qeRow).keydown(function(e){
  85. if ( e.which === 13 && ! $( e.target ).hasClass( 'cancel' ) ) {
  86. return inlineEditPost.save(this);
  87. }
  88. });
  89. /**
  90. * Reverts changes and close the bulk editor if the cancel button is clicked.
  91. *
  92. * @returns {boolean} The result of revert.
  93. */
  94. $( '.cancel', bulkRow ).click( function() {
  95. return inlineEditPost.revert();
  96. });
  97. /**
  98. * Disables the password input field when the private post checkbox is checked.
  99. */
  100. $('#inline-edit .inline-edit-private input[value="private"]').click( function(){
  101. var pw = $('input.inline-edit-password-input');
  102. if ( $(this).prop('checked') ) {
  103. pw.val('').prop('disabled', true);
  104. } else {
  105. pw.prop('disabled', false);
  106. }
  107. });
  108. /**
  109. * Binds click event to the .editinline button which opens the quick editor.
  110. */
  111. $( '#the-list' ).on( 'click', '.editinline', function() {
  112. $( this ).attr( 'aria-expanded', 'true' );
  113. inlineEditPost.edit( this );
  114. });
  115. $('#bulk-edit').find('fieldset:first').after(
  116. $('#inline-edit fieldset.inline-edit-categories').clone()
  117. ).siblings( 'fieldset:last' ).prepend(
  118. $('#inline-edit label.inline-edit-tags').clone()
  119. );
  120. $('select[name="_status"] option[value="future"]', bulkRow).remove();
  121. /**
  122. * Adds onclick events to the apply buttons.
  123. */
  124. $('#doaction, #doaction2').click(function(e){
  125. var n;
  126. t.whichBulkButtonId = $( this ).attr( 'id' );
  127. n = t.whichBulkButtonId.substr( 2 );
  128. if ( 'edit' === $( 'select[name="' + n + '"]' ).val() ) {
  129. e.preventDefault();
  130. t.setBulk();
  131. } else if ( $('form#posts-filter tr.inline-editor').length > 0 ) {
  132. t.revert();
  133. }
  134. });
  135. },
  136. /**
  137. * Toggles the quick edit window, hiding it when it's active and showing it when
  138. * inactive.
  139. *
  140. * @memberof inlineEditPost
  141. * @since 2.7.0
  142. *
  143. * @param {Object} el Element within a post table row.
  144. */
  145. toggle : function(el){
  146. var t = this;
  147. $( t.what + t.getId( el ) ).css( 'display' ) === 'none' ? t.revert() : t.edit( el );
  148. },
  149. /**
  150. * Creates the bulk editor row to edit multiple posts at once.
  151. *
  152. * @memberof inlineEditPost
  153. * @since 2.7.0
  154. */
  155. setBulk : function(){
  156. var te = '', type = this.type, c = true;
  157. this.revert();
  158. $( '#bulk-edit td' ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length );
  159. // Insert the editor at the top of the table with an empty row above to maintain zebra striping.
  160. $('table.widefat tbody').prepend( $('#bulk-edit') ).prepend('<tr class="hidden"></tr>');
  161. $('#bulk-edit').addClass('inline-editor').show();
  162. /**
  163. * Create a HTML div with the title and a link(delete-icon) for each selected
  164. * post.
  165. *
  166. * Get the selected posts based on the checked checkboxes in the post table.
  167. */
  168. $( 'tbody th.check-column input[type="checkbox"]' ).each( function() {
  169. // If the checkbox for a post is selected, add the post to the edit list.
  170. if ( $(this).prop('checked') ) {
  171. c = false;
  172. var id = $(this).val(), theTitle;
  173. theTitle = $('#inline_'+id+' .post_title').html() || inlineEditL10n.notitle;
  174. te += '<div id="ttle'+id+'"><a id="_'+id+'" class="ntdelbutton" title="'+inlineEditL10n.ntdeltitle+'">X</a>'+theTitle+'</div>';
  175. }
  176. });
  177. // If no checkboxes where checked, just hide the quick/bulk edit rows.
  178. if ( c ) {
  179. return this.revert();
  180. }
  181. // Add onclick events to the delete-icons in the bulk editors the post title list.
  182. $('#bulk-titles').html(te);
  183. /**
  184. * Binds on click events to the checkboxes before the posts in the table.
  185. *
  186. * @listens click
  187. */
  188. $('#bulk-titles a').click(function(){
  189. var id = $(this).attr('id').substr(1);
  190. $('table.widefat input[value="' + id + '"]').prop('checked', false);
  191. $('#ttle'+id).remove();
  192. });
  193. // Enable auto-complete for tags when editing posts.
  194. if ( 'post' === type ) {
  195. $( 'tr.inline-editor textarea[data-wp-taxonomy]' ).each( function ( i, element ) {
  196. /*
  197. * While Quick Edit clones the form each time, Bulk Edit always re-uses
  198. * the same form. Let's check if an autocomplete instance already exists.
  199. */
  200. if ( $( element ).autocomplete( 'instance' ) ) {
  201. // jQuery equivalent of `continue` within an `each()` loop.
  202. return;
  203. }
  204. $( element ).wpTagsSuggest();
  205. } );
  206. }
  207. // Scrolls to the top of the table where the editor is rendered.
  208. $('html, body').animate( { scrollTop: 0 }, 'fast' );
  209. },
  210. /**
  211. * Creates a quick edit window for the post that has been clicked.
  212. *
  213. * @memberof inlineEditPost
  214. * @since 2.7.0
  215. *
  216. * @param {number|Object} id The id of the clicked post or an element within a post
  217. * table row.
  218. * @returns {boolean} Always returns false at the end of execution.
  219. */
  220. edit : function(id) {
  221. var t = this, fields, editRow, rowData, status, pageOpt, pageLevel, nextPage, pageLoop = true, nextLevel, f, val, pw;
  222. t.revert();
  223. if ( typeof(id) === 'object' ) {
  224. id = t.getId(id);
  225. }
  226. fields = ['post_title', 'post_name', 'post_author', '_status', 'jj', 'mm', 'aa', 'hh', 'mn', 'ss', 'post_password', 'post_format', 'menu_order', 'page_template'];
  227. if ( t.type === 'page' ) {
  228. fields.push('post_parent');
  229. }
  230. // Add the new edit row with an extra blank row underneath to maintain zebra striping.
  231. editRow = $('#inline-edit').clone(true);
  232. $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length );
  233. $(t.what+id).removeClass('is-expanded').hide().after(editRow).after('<tr class="hidden"></tr>');
  234. // Populate fields in the quick edit window.
  235. rowData = $('#inline_'+id);
  236. if ( !$(':input[name="post_author"] option[value="' + $('.post_author', rowData).text() + '"]', editRow).val() ) {
  237. // The post author no longer has edit capabilities, so we need to add them to the list of authors.
  238. $(':input[name="post_author"]', editRow).prepend('<option value="' + $('.post_author', rowData).text() + '">' + $('#' + t.type + '-' + id + ' .author').text() + '</option>');
  239. }
  240. if ( $( ':input[name="post_author"] option', editRow ).length === 1 ) {
  241. $('label.inline-edit-author', editRow).hide();
  242. }
  243. for ( f = 0; f < fields.length; f++ ) {
  244. val = $('.'+fields[f], rowData);
  245. /**
  246. * Replaces the image for a Twemoji(Twitter emoji) with it's alternate text.
  247. *
  248. * @returns Alternate text from the image.
  249. */
  250. val.find( 'img' ).replaceWith( function() { return this.alt; } );
  251. val = val.text();
  252. $(':input[name="' + fields[f] + '"]', editRow).val( val );
  253. }
  254. if ( $( '.comment_status', rowData ).text() === 'open' ) {
  255. $( 'input[name="comment_status"]', editRow ).prop( 'checked', true );
  256. }
  257. if ( $( '.ping_status', rowData ).text() === 'open' ) {
  258. $( 'input[name="ping_status"]', editRow ).prop( 'checked', true );
  259. }
  260. if ( $( '.sticky', rowData ).text() === 'sticky' ) {
  261. $( 'input[name="sticky"]', editRow ).prop( 'checked', true );
  262. }
  263. /**
  264. * Creates the select boxes for the categories.
  265. */
  266. $('.post_category', rowData).each(function(){
  267. var taxname,
  268. term_ids = $(this).text();
  269. if ( term_ids ) {
  270. taxname = $(this).attr('id').replace('_'+id, '');
  271. $('ul.'+taxname+'-checklist :checkbox', editRow).val(term_ids.split(','));
  272. }
  273. });
  274. /**
  275. * Gets all the taxonomies for live auto-fill suggestions when typing the name
  276. * of a tag.
  277. */
  278. $('.tags_input', rowData).each(function(){
  279. var terms = $(this),
  280. taxname = $(this).attr('id').replace('_' + id, ''),
  281. textarea = $('textarea.tax_input_' + taxname, editRow),
  282. comma = inlineEditL10n.comma;
  283. terms.find( 'img' ).replaceWith( function() { return this.alt; } );
  284. terms = terms.text();
  285. if ( terms ) {
  286. if ( ',' !== comma ) {
  287. terms = terms.replace(/,/g, comma);
  288. }
  289. textarea.val(terms);
  290. }
  291. textarea.wpTagsSuggest();
  292. });
  293. // Handle the post status.
  294. status = $('._status', rowData).text();
  295. if ( 'future' !== status ) {
  296. $('select[name="_status"] option[value="future"]', editRow).remove();
  297. }
  298. pw = $( '.inline-edit-password-input' ).prop( 'disabled', false );
  299. if ( 'private' === status ) {
  300. $('input[name="keep_private"]', editRow).prop('checked', true);
  301. pw.val( '' ).prop( 'disabled', true );
  302. }
  303. // Remove the current page and children from the parent dropdown.
  304. pageOpt = $('select[name="post_parent"] option[value="' + id + '"]', editRow);
  305. if ( pageOpt.length > 0 ) {
  306. pageLevel = pageOpt[0].className.split('-')[1];
  307. nextPage = pageOpt;
  308. while ( pageLoop ) {
  309. nextPage = nextPage.next('option');
  310. if ( nextPage.length === 0 ) {
  311. break;
  312. }
  313. nextLevel = nextPage[0].className.split('-')[1];
  314. if ( nextLevel <= pageLevel ) {
  315. pageLoop = false;
  316. } else {
  317. nextPage.remove();
  318. nextPage = pageOpt;
  319. }
  320. }
  321. pageOpt.remove();
  322. }
  323. $(editRow).attr('id', 'edit-'+id).addClass('inline-editor').show();
  324. $('.ptitle', editRow).focus();
  325. return false;
  326. },
  327. /**
  328. * Saves the changes made in the quick edit window to the post.
  329. * AJAX saving is only for Quick Edit and not for bulk edit.
  330. *
  331. * @since 2.7.0
  332. *
  333. * @param {int} id The id for the post that has been changed.
  334. * @returns {boolean} false, so the form does not submit when pressing
  335. * Enter on a focused field.
  336. */
  337. save : function(id) {
  338. var params, fields, page = $('.post_status_page').val() || '';
  339. if ( typeof(id) === 'object' ) {
  340. id = this.getId(id);
  341. }
  342. $( 'table.widefat .spinner' ).addClass( 'is-active' );
  343. params = {
  344. action: 'inline-save',
  345. post_type: typenow,
  346. post_ID: id,
  347. edit_date: 'true',
  348. post_status: page
  349. };
  350. fields = $('#edit-'+id).find(':input').serialize();
  351. params = fields + '&' + $.param(params);
  352. // Make ajax request.
  353. $.post( ajaxurl, params,
  354. function(r) {
  355. var $errorNotice = $( '#edit-' + id + ' .inline-edit-save .notice-error' ),
  356. $error = $errorNotice.find( '.error' );
  357. $( 'table.widefat .spinner' ).removeClass( 'is-active' );
  358. $( '.ac_results' ).hide();
  359. if (r) {
  360. if ( -1 !== r.indexOf( '<tr' ) ) {
  361. $(inlineEditPost.what+id).siblings('tr.hidden').addBack().remove();
  362. $('#edit-'+id).before(r).remove();
  363. $( inlineEditPost.what + id ).hide().fadeIn( 400, function() {
  364. // Move focus back to the Quick Edit button. $( this ) is the row being animated.
  365. $( this ).find( '.editinline' )
  366. .attr( 'aria-expanded', 'false' )
  367. .focus();
  368. wp.a11y.speak( inlineEditL10n.saved );
  369. });
  370. } else {
  371. r = r.replace( /<.[^<>]*?>/g, '' );
  372. $errorNotice.removeClass( 'hidden' );
  373. $error.html( r );
  374. wp.a11y.speak( $error.text() );
  375. }
  376. } else {
  377. $errorNotice.removeClass( 'hidden' );
  378. $error.html( inlineEditL10n.error );
  379. wp.a11y.speak( inlineEditL10n.error );
  380. }
  381. },
  382. 'html');
  383. // Prevent submitting the form when pressing Enter on a focused field.
  384. return false;
  385. },
  386. /**
  387. * Hides and empties the Quick Edit and/or Bulk Edit windows.
  388. *
  389. * @memberof inlineEditPost
  390. * @since 2.7.0
  391. *
  392. * @returns {boolean} Always returns false.
  393. */
  394. revert : function(){
  395. var $tableWideFat = $( '.widefat' ),
  396. id = $( '.inline-editor', $tableWideFat ).attr( 'id' );
  397. if ( id ) {
  398. $( '.spinner', $tableWideFat ).removeClass( 'is-active' );
  399. $( '.ac_results' ).hide();
  400. if ( 'bulk-edit' === id ) {
  401. // Hide the bulk editor.
  402. $( '#bulk-edit', $tableWideFat ).removeClass( 'inline-editor' ).hide().siblings( '.hidden' ).remove();
  403. $('#bulk-titles').empty();
  404. // Store the empty bulk editor in a hidden element.
  405. $('#inlineedit').append( $('#bulk-edit') );
  406. // Move focus back to the Bulk Action button that was activated.
  407. $( '#' + inlineEditPost.whichBulkButtonId ).focus();
  408. } else {
  409. // Remove both the inline-editor and its hidden tr siblings.
  410. $('#'+id).siblings('tr.hidden').addBack().remove();
  411. id = id.substr( id.lastIndexOf('-') + 1 );
  412. // Show the post row and move focus back to the Quick Edit button.
  413. $( this.what + id ).show().find( '.editinline' )
  414. .attr( 'aria-expanded', 'false' )
  415. .focus();
  416. }
  417. }
  418. return false;
  419. },
  420. /**
  421. * Gets the id for a the post that you want to quick edit from the row in the quick
  422. * edit table.
  423. *
  424. * @memberof inlineEditPost
  425. * @since 2.7.0
  426. *
  427. * @param {Object} o DOM row object to get the id for.
  428. * @returns {string} The post id extracted from the table row in the object.
  429. */
  430. getId : function(o) {
  431. var id = $(o).closest('tr').attr('id'),
  432. parts = id.split('-');
  433. return parts[parts.length - 1];
  434. }
  435. };
  436. $( document ).ready( function(){ inlineEditPost.init(); } );
  437. // Show/hide locks on posts.
  438. $( document ).on( 'heartbeat-tick.wp-check-locked-posts', function( e, data ) {
  439. var locked = data['wp-check-locked-posts'] || {};
  440. $('#the-list tr').each( function(i, el) {
  441. var key = el.id, row = $(el), lock_data, avatar;
  442. if ( locked.hasOwnProperty( key ) ) {
  443. if ( ! row.hasClass('wp-locked') ) {
  444. lock_data = locked[key];
  445. row.find('.column-title .locked-text').text( lock_data.text );
  446. row.find('.check-column checkbox').prop('checked', false);
  447. if ( lock_data.avatar_src ) {
  448. avatar = $( '<img class="avatar avatar-18 photo" width="18" height="18" alt="" />' ).attr( 'src', lock_data.avatar_src.replace( /&amp;/g, '&' ) );
  449. row.find('.column-title .locked-avatar').empty().append( avatar );
  450. }
  451. row.addClass('wp-locked');
  452. }
  453. } else if ( row.hasClass('wp-locked') ) {
  454. row.removeClass( 'wp-locked' ).find( '.locked-info span' ).empty();
  455. }
  456. });
  457. }).on( 'heartbeat-send.wp-check-locked-posts', function( e, data ) {
  458. var check = [];
  459. $('#the-list tr').each( function(i, el) {
  460. if ( el.id ) {
  461. check.push( el.id );
  462. }
  463. });
  464. if ( check.length ) {
  465. data['wp-check-locked-posts'] = check;
  466. }
  467. }).ready( function() {
  468. // Set the heartbeat interval to 15 sec.
  469. if ( typeof wp !== 'undefined' && wp.heartbeat ) {
  470. wp.heartbeat.interval( 15 );
  471. }
  472. });
  473. })( jQuery, window.wp );