post.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273
  1. /**
  2. * @file Contains all dynamic functionality needed on post and term pages.
  3. *
  4. * @output wp-admin/js/post.js
  5. */
  6. /* global postL10n, ajaxurl, wpAjax, setPostThumbnailL10n, postboxes, pagenow, tinymce, alert, deleteUserSetting */
  7. /* global theList:true, theExtraList:true, getUserSetting, setUserSetting, commentReply, commentsBox */
  8. /* global WPSetThumbnailHTML, wptitlehint */
  9. // Backwards compatibility: prevent fatal errors.
  10. window.makeSlugeditClickable = window.editPermalink = function(){};
  11. // Make sure the wp object exists.
  12. window.wp = window.wp || {};
  13. ( function( $ ) {
  14. var titleHasFocus = false;
  15. /**
  16. * Control loading of comments on the post and term edit pages.
  17. *
  18. * @type {{st: number, get: commentsBox.get, load: commentsBox.load}}
  19. *
  20. * @namespace commentsBox
  21. */
  22. window.commentsBox = {
  23. // Comment offset to use when fetching new comments.
  24. st : 0,
  25. /**
  26. * Fetch comments using AJAX and display them in the box.
  27. *
  28. * @memberof commentsBox
  29. *
  30. * @param {int} total Total number of comments for this post.
  31. * @param {int} num Optional. Number of comments to fetch, defaults to 20.
  32. * @returns {boolean} Always returns false.
  33. */
  34. get : function(total, num) {
  35. var st = this.st, data;
  36. if ( ! num )
  37. num = 20;
  38. this.st += num;
  39. this.total = total;
  40. $( '#commentsdiv .spinner' ).addClass( 'is-active' );
  41. data = {
  42. 'action' : 'get-comments',
  43. 'mode' : 'single',
  44. '_ajax_nonce' : $('#add_comment_nonce').val(),
  45. 'p' : $('#post_ID').val(),
  46. 'start' : st,
  47. 'number' : num
  48. };
  49. $.post(
  50. ajaxurl,
  51. data,
  52. function(r) {
  53. r = wpAjax.parseAjaxResponse(r);
  54. $('#commentsdiv .widefat').show();
  55. $( '#commentsdiv .spinner' ).removeClass( 'is-active' );
  56. if ( 'object' == typeof r && r.responses[0] ) {
  57. $('#the-comment-list').append( r.responses[0].data );
  58. theList = theExtraList = null;
  59. $( 'a[className*=\':\']' ).unbind();
  60. // If the offset is over the total number of comments we cannot fetch any more, so hide the button.
  61. if ( commentsBox.st > commentsBox.total )
  62. $('#show-comments').hide();
  63. else
  64. $('#show-comments').show().children('a').html(postL10n.showcomm);
  65. return;
  66. } else if ( 1 == r ) {
  67. $('#show-comments').html(postL10n.endcomm);
  68. return;
  69. }
  70. $('#the-comment-list').append('<tr><td colspan="2">'+wpAjax.broken+'</td></tr>');
  71. }
  72. );
  73. return false;
  74. },
  75. /**
  76. * Load the next batch of comments.
  77. *
  78. * @param {int} total Total number of comments to load.
  79. *
  80. * @memberof commentsBox
  81. */
  82. load: function(total){
  83. this.st = jQuery('#the-comment-list tr.comment:visible').length;
  84. this.get(total);
  85. }
  86. };
  87. /**
  88. * Overwrite the content of the Featured Image postbox
  89. *
  90. * @param {string} html New HTML to be displayed in the content area of the postbox.
  91. *
  92. * @global
  93. */
  94. window.WPSetThumbnailHTML = function(html){
  95. $('.inside', '#postimagediv').html(html);
  96. };
  97. /**
  98. * Set the Image ID of the Featured Image
  99. *
  100. * @param {int} id The post_id of the image to use as Featured Image.
  101. *
  102. * @global
  103. */
  104. window.WPSetThumbnailID = function(id){
  105. var field = $('input[value="_thumbnail_id"]', '#list-table');
  106. if ( field.length > 0 ) {
  107. $('#meta\\[' + field.attr('id').match(/[0-9]+/) + '\\]\\[value\\]').text(id);
  108. }
  109. };
  110. /**
  111. * Remove the Featured Image
  112. *
  113. * @param {string} nonce Nonce to use in the request.
  114. *
  115. * @global
  116. */
  117. window.WPRemoveThumbnail = function(nonce){
  118. $.post(ajaxurl, {
  119. action: 'set-post-thumbnail', post_id: $( '#post_ID' ).val(), thumbnail_id: -1, _ajax_nonce: nonce, cookie: encodeURIComponent( document.cookie )
  120. },
  121. /**
  122. * Handle server response
  123. *
  124. * @param {string} str Response, will be '0' when an error occurred otherwise contains link to add Featured Image.
  125. */
  126. function(str){
  127. if ( str == '0' ) {
  128. alert( setPostThumbnailL10n.error );
  129. } else {
  130. WPSetThumbnailHTML(str);
  131. }
  132. }
  133. );
  134. };
  135. /**
  136. * Heartbeat locks.
  137. *
  138. * Used to lock editing of an object by only one user at a time.
  139. *
  140. * When the user does not send a heartbeat in a heartbeat-time
  141. * the user is no longer editing and another user can start editing.
  142. */
  143. $(document).on( 'heartbeat-send.refresh-lock', function( e, data ) {
  144. var lock = $('#active_post_lock').val(),
  145. post_id = $('#post_ID').val(),
  146. send = {};
  147. if ( ! post_id || ! $('#post-lock-dialog').length )
  148. return;
  149. send.post_id = post_id;
  150. if ( lock )
  151. send.lock = lock;
  152. data['wp-refresh-post-lock'] = send;
  153. }).on( 'heartbeat-tick.refresh-lock', function( e, data ) {
  154. // Post locks: update the lock string or show the dialog if somebody has taken over editing.
  155. var received, wrap, avatar;
  156. if ( data['wp-refresh-post-lock'] ) {
  157. received = data['wp-refresh-post-lock'];
  158. if ( received.lock_error ) {
  159. // Show "editing taken over" message.
  160. wrap = $('#post-lock-dialog');
  161. if ( wrap.length && ! wrap.is(':visible') ) {
  162. if ( wp.autosave ) {
  163. // Save the latest changes and disable.
  164. $(document).one( 'heartbeat-tick', function() {
  165. wp.autosave.server.suspend();
  166. wrap.removeClass('saving').addClass('saved');
  167. $(window).off( 'beforeunload.edit-post' );
  168. });
  169. wrap.addClass('saving');
  170. wp.autosave.server.triggerSave();
  171. }
  172. if ( received.lock_error.avatar_src ) {
  173. avatar = $( '<img class="avatar avatar-64 photo" width="64" height="64" alt="" />' ).attr( 'src', received.lock_error.avatar_src.replace( /&amp;/g, '&' ) );
  174. wrap.find('div.post-locked-avatar').empty().append( avatar );
  175. }
  176. wrap.show().find('.currently-editing').text( received.lock_error.text );
  177. wrap.find('.wp-tab-first').focus();
  178. }
  179. } else if ( received.new_lock ) {
  180. $('#active_post_lock').val( received.new_lock );
  181. }
  182. }
  183. }).on( 'before-autosave.update-post-slug', function() {
  184. titleHasFocus = document.activeElement && document.activeElement.id === 'title';
  185. }).on( 'after-autosave.update-post-slug', function() {
  186. /*
  187. * Create slug area only if not already there
  188. * and the title field was not focused (user was not typing a title) when autosave ran.
  189. */
  190. if ( ! $('#edit-slug-box > *').length && ! titleHasFocus ) {
  191. $.post( ajaxurl, {
  192. action: 'sample-permalink',
  193. post_id: $('#post_ID').val(),
  194. new_title: $('#title').val(),
  195. samplepermalinknonce: $('#samplepermalinknonce').val()
  196. },
  197. function( data ) {
  198. if ( data != '-1' ) {
  199. $('#edit-slug-box').html(data);
  200. }
  201. }
  202. );
  203. }
  204. });
  205. }(jQuery));
  206. /**
  207. * Heartbeat refresh nonces.
  208. */
  209. (function($) {
  210. var check, timeout;
  211. /**
  212. * Only allow to check for nonce refresh every 30 seconds.
  213. */
  214. function schedule() {
  215. check = false;
  216. window.clearTimeout( timeout );
  217. timeout = window.setTimeout( function(){ check = true; }, 300000 );
  218. }
  219. $(document).on( 'heartbeat-send.wp-refresh-nonces', function( e, data ) {
  220. var post_id,
  221. $authCheck = $('#wp-auth-check-wrap');
  222. if ( check || ( $authCheck.length && ! $authCheck.hasClass( 'hidden' ) ) ) {
  223. if ( ( post_id = $('#post_ID').val() ) && $('#_wpnonce').val() ) {
  224. data['wp-refresh-post-nonces'] = {
  225. post_id: post_id
  226. };
  227. }
  228. }
  229. }).on( 'heartbeat-tick.wp-refresh-nonces', function( e, data ) {
  230. var nonces = data['wp-refresh-post-nonces'];
  231. if ( nonces ) {
  232. schedule();
  233. if ( nonces.replace ) {
  234. $.each( nonces.replace, function( selector, value ) {
  235. $( '#' + selector ).val( value );
  236. });
  237. }
  238. if ( nonces.heartbeatNonce )
  239. window.heartbeatSettings.nonce = nonces.heartbeatNonce;
  240. }
  241. }).ready( function() {
  242. schedule();
  243. });
  244. }(jQuery));
  245. /**
  246. * All post and postbox controls and functionality.
  247. */
  248. jQuery(document).ready( function($) {
  249. var stamp, visibility, $submitButtons, updateVisibility, updateText,
  250. sticky = '',
  251. $textarea = $('#content'),
  252. $document = $(document),
  253. postId = $('#post_ID').val() || 0,
  254. $submitpost = $('#submitpost'),
  255. releaseLock = true,
  256. $postVisibilitySelect = $('#post-visibility-select'),
  257. $timestampdiv = $('#timestampdiv'),
  258. $postStatusSelect = $('#post-status-select'),
  259. isMac = window.navigator.platform ? window.navigator.platform.indexOf( 'Mac' ) !== -1 : false;
  260. postboxes.add_postbox_toggles(pagenow);
  261. /*
  262. * Clear the window name. Otherwise if this is a former preview window where the user navigated to edit another post,
  263. * and the first post is still being edited, clicking Preview there will use this window to show the preview.
  264. */
  265. window.name = '';
  266. // Post locks: contain focus inside the dialog. If the dialog is shown, focus the first item.
  267. $('#post-lock-dialog .notification-dialog').on( 'keydown', function(e) {
  268. // Don't do anything when [tab] is pressed.
  269. if ( e.which != 9 )
  270. return;
  271. var target = $(e.target);
  272. // [shift] + [tab] on first tab cycles back to last tab.
  273. if ( target.hasClass('wp-tab-first') && e.shiftKey ) {
  274. $(this).find('.wp-tab-last').focus();
  275. e.preventDefault();
  276. // [tab] on last tab cycles back to first tab.
  277. } else if ( target.hasClass('wp-tab-last') && ! e.shiftKey ) {
  278. $(this).find('.wp-tab-first').focus();
  279. e.preventDefault();
  280. }
  281. }).filter(':visible').find('.wp-tab-first').focus();
  282. // Set the heartbeat interval to 15 sec. if post lock dialogs are enabled.
  283. if ( wp.heartbeat && $('#post-lock-dialog').length ) {
  284. wp.heartbeat.interval( 15 );
  285. }
  286. // The form is being submitted by the user.
  287. $submitButtons = $submitpost.find( ':submit, a.submitdelete, #post-preview' ).on( 'click.edit-post', function( event ) {
  288. var $button = $(this);
  289. if ( $button.hasClass('disabled') ) {
  290. event.preventDefault();
  291. return;
  292. }
  293. if ( $button.hasClass('submitdelete') || $button.is( '#post-preview' ) ) {
  294. return;
  295. }
  296. // The form submission can be blocked from JS or by using HTML 5.0 validation on some fields.
  297. // Run this only on an actual 'submit'.
  298. $('form#post').off( 'submit.edit-post' ).on( 'submit.edit-post', function( event ) {
  299. if ( event.isDefaultPrevented() ) {
  300. return;
  301. }
  302. // Stop auto save.
  303. if ( wp.autosave ) {
  304. wp.autosave.server.suspend();
  305. }
  306. if ( typeof commentReply !== 'undefined' ) {
  307. /*
  308. * Warn the user they have an unsaved comment before submitting
  309. * the post data for update.
  310. */
  311. if ( ! commentReply.discardCommentChanges() ) {
  312. return false;
  313. }
  314. /*
  315. * Close the comment edit/reply form if open to stop the form
  316. * action from interfering with the post's form action.
  317. */
  318. commentReply.close();
  319. }
  320. releaseLock = false;
  321. $(window).off( 'beforeunload.edit-post' );
  322. $submitButtons.addClass( 'disabled' );
  323. if ( $button.attr('id') === 'publish' ) {
  324. $submitpost.find( '#major-publishing-actions .spinner' ).addClass( 'is-active' );
  325. } else {
  326. $submitpost.find( '#minor-publishing .spinner' ).addClass( 'is-active' );
  327. }
  328. });
  329. });
  330. // Submit the form saving a draft or an autosave, and show a preview in a new tab
  331. $('#post-preview').on( 'click.post-preview', function( event ) {
  332. var $this = $(this),
  333. $form = $('form#post'),
  334. $previewField = $('input#wp-preview'),
  335. target = $this.attr('target') || 'wp-preview',
  336. ua = navigator.userAgent.toLowerCase();
  337. event.preventDefault();
  338. if ( $this.hasClass('disabled') ) {
  339. return;
  340. }
  341. if ( wp.autosave ) {
  342. wp.autosave.server.tempBlockSave();
  343. }
  344. $previewField.val('dopreview');
  345. $form.attr( 'target', target ).submit().attr( 'target', '' );
  346. // Workaround for WebKit bug preventing a form submitting twice to the same action.
  347. // https://bugs.webkit.org/show_bug.cgi?id=28633
  348. if ( ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1 ) {
  349. $form.attr( 'action', function( index, value ) {
  350. return value + '?t=' + ( new Date() ).getTime();
  351. });
  352. }
  353. $previewField.val('');
  354. });
  355. // This code is meant to allow tabbing from Title to Post content.
  356. $('#title').on( 'keydown.editor-focus', function( event ) {
  357. var editor;
  358. if ( event.keyCode === 9 && ! event.ctrlKey && ! event.altKey && ! event.shiftKey ) {
  359. editor = typeof tinymce != 'undefined' && tinymce.get('content');
  360. if ( editor && ! editor.isHidden() ) {
  361. editor.focus();
  362. } else if ( $textarea.length ) {
  363. $textarea.focus();
  364. } else {
  365. return;
  366. }
  367. event.preventDefault();
  368. }
  369. });
  370. // Auto save new posts after a title is typed.
  371. if ( $( '#auto_draft' ).val() ) {
  372. $( '#title' ).blur( function() {
  373. var cancel;
  374. if ( ! this.value || $('#edit-slug-box > *').length ) {
  375. return;
  376. }
  377. // Cancel the auto save when the blur was triggered by the user submitting the form.
  378. $('form#post').one( 'submit', function() {
  379. cancel = true;
  380. });
  381. window.setTimeout( function() {
  382. if ( ! cancel && wp.autosave ) {
  383. wp.autosave.server.triggerSave();
  384. }
  385. }, 200 );
  386. });
  387. }
  388. $document.on( 'autosave-disable-buttons.edit-post', function() {
  389. $submitButtons.addClass( 'disabled' );
  390. }).on( 'autosave-enable-buttons.edit-post', function() {
  391. if ( ! wp.heartbeat || ! wp.heartbeat.hasConnectionError() ) {
  392. $submitButtons.removeClass( 'disabled' );
  393. }
  394. }).on( 'before-autosave.edit-post', function() {
  395. $( '.autosave-message' ).text( postL10n.savingText );
  396. }).on( 'after-autosave.edit-post', function( event, data ) {
  397. $( '.autosave-message' ).text( data.message );
  398. if ( $( document.body ).hasClass( 'post-new-php' ) ) {
  399. $( '.submitbox .submitdelete' ).show();
  400. }
  401. });
  402. /*
  403. * When the user is trying to load another page, or reloads current page
  404. * show a confirmation dialog when there are unsaved changes.
  405. */
  406. $(window).on( 'beforeunload.edit-post', function() {
  407. var editor = typeof tinymce !== 'undefined' && tinymce.get('content');
  408. if ( ( editor && ! editor.isHidden() && editor.isDirty() ) ||
  409. ( wp.autosave && wp.autosave.server.postChanged() ) ) {
  410. return postL10n.saveAlert;
  411. }
  412. }).on( 'unload.edit-post', function( event ) {
  413. if ( ! releaseLock ) {
  414. return;
  415. }
  416. /*
  417. * Unload is triggered (by hand) on removing the Thickbox iframe.
  418. * Make sure we process only the main document unload.
  419. */
  420. if ( event.target && event.target.nodeName != '#document' ) {
  421. return;
  422. }
  423. var postID = $('#post_ID').val();
  424. var postLock = $('#active_post_lock').val();
  425. if ( ! postID || ! postLock ) {
  426. return;
  427. }
  428. var data = {
  429. action: 'wp-remove-post-lock',
  430. _wpnonce: $('#_wpnonce').val(),
  431. post_ID: postID,
  432. active_post_lock: postLock
  433. };
  434. if ( window.FormData && window.navigator.sendBeacon ) {
  435. var formData = new window.FormData();
  436. $.each( data, function( key, value ) {
  437. formData.append( key, value );
  438. });
  439. if ( window.navigator.sendBeacon( ajaxurl, formData ) ) {
  440. return;
  441. }
  442. }
  443. // Fall back to a synchronous POST request.
  444. // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
  445. $.post({
  446. async: false,
  447. data: data,
  448. url: ajaxurl
  449. });
  450. });
  451. // Multiple Taxonomies.
  452. if ( $('#tagsdiv-post_tag').length ) {
  453. window.tagBox && window.tagBox.init();
  454. } else {
  455. $('.meta-box-sortables').children('div.postbox').each(function(){
  456. if ( this.id.indexOf('tagsdiv-') === 0 ) {
  457. window.tagBox && window.tagBox.init();
  458. return false;
  459. }
  460. });
  461. }
  462. // Handle categories.
  463. $('.categorydiv').each( function(){
  464. var this_id = $(this).attr('id'), catAddBefore, catAddAfter, taxonomyParts, taxonomy, settingName;
  465. taxonomyParts = this_id.split('-');
  466. taxonomyParts.shift();
  467. taxonomy = taxonomyParts.join('-');
  468. settingName = taxonomy + '_tab';
  469. if ( taxonomy == 'category' ) {
  470. settingName = 'cats';
  471. }
  472. // TODO: move to jQuery 1.3+, support for multiple hierarchical taxonomies, see wp-lists.js
  473. $('a', '#' + taxonomy + '-tabs').click( function( e ) {
  474. e.preventDefault();
  475. var t = $(this).attr('href');
  476. $(this).parent().addClass('tabs').siblings('li').removeClass('tabs');
  477. $('#' + taxonomy + '-tabs').siblings('.tabs-panel').hide();
  478. $(t).show();
  479. if ( '#' + taxonomy + '-all' == t ) {
  480. deleteUserSetting( settingName );
  481. } else {
  482. setUserSetting( settingName, 'pop' );
  483. }
  484. });
  485. if ( getUserSetting( settingName ) )
  486. $('a[href="#' + taxonomy + '-pop"]', '#' + taxonomy + '-tabs').click();
  487. // Add category button controls.
  488. $('#new' + taxonomy).one( 'focus', function() {
  489. $( this ).val( '' ).removeClass( 'form-input-tip' );
  490. });
  491. // On [enter] submit the taxonomy.
  492. $('#new' + taxonomy).keypress( function(event){
  493. if( 13 === event.keyCode ) {
  494. event.preventDefault();
  495. $('#' + taxonomy + '-add-submit').click();
  496. }
  497. });
  498. // After submitting a new taxonomy, re-focus the input field.
  499. $('#' + taxonomy + '-add-submit').click( function() {
  500. $('#new' + taxonomy).focus();
  501. });
  502. /**
  503. * Before adding a new taxonomy, disable submit button.
  504. *
  505. * @param {Object} s Taxonomy object which will be added.
  506. *
  507. * @returns {Object}
  508. */
  509. catAddBefore = function( s ) {
  510. if ( !$('#new'+taxonomy).val() ) {
  511. return false;
  512. }
  513. s.data += '&' + $( ':checked', '#'+taxonomy+'checklist' ).serialize();
  514. $( '#' + taxonomy + '-add-submit' ).prop( 'disabled', true );
  515. return s;
  516. };
  517. /**
  518. * Re-enable submit button after a taxonomy has been added.
  519. *
  520. * Re-enable submit button.
  521. * If the taxonomy has a parent place the taxonomy underneath the parent.
  522. *
  523. * @param {Object} r Response.
  524. * @param {Object} s Taxonomy data.
  525. *
  526. * @returns void
  527. */
  528. catAddAfter = function( r, s ) {
  529. var sup, drop = $('#new'+taxonomy+'_parent');
  530. $( '#' + taxonomy + '-add-submit' ).prop( 'disabled', false );
  531. if ( 'undefined' != s.parsed.responses[0] && (sup = s.parsed.responses[0].supplemental.newcat_parent) ) {
  532. drop.before(sup);
  533. drop.remove();
  534. }
  535. };
  536. $('#' + taxonomy + 'checklist').wpList({
  537. alt: '',
  538. response: taxonomy + '-ajax-response',
  539. addBefore: catAddBefore,
  540. addAfter: catAddAfter
  541. });
  542. // Add new taxonomy button toggles input form visibility.
  543. $('#' + taxonomy + '-add-toggle').click( function( e ) {
  544. e.preventDefault();
  545. $('#' + taxonomy + '-adder').toggleClass( 'wp-hidden-children' );
  546. $('a[href="#' + taxonomy + '-all"]', '#' + taxonomy + '-tabs').click();
  547. $('#new'+taxonomy).focus();
  548. });
  549. // Sync checked items between "All {taxonomy}" and "Most used" lists.
  550. $('#' + taxonomy + 'checklist, #' + taxonomy + 'checklist-pop').on( 'click', 'li.popular-category > label input[type="checkbox"]', function() {
  551. var t = $(this), c = t.is(':checked'), id = t.val();
  552. if ( id && t.parents('#taxonomy-'+taxonomy).length )
  553. $('#in-' + taxonomy + '-' + id + ', #in-popular-' + taxonomy + '-' + id).prop( 'checked', c );
  554. });
  555. }); // end cats
  556. // Custom Fields postbox.
  557. if ( $('#postcustom').length ) {
  558. $( '#the-list' ).wpList( {
  559. /**
  560. * Add current post_ID to request to fetch custom fields
  561. *
  562. * @ignore
  563. *
  564. * @param {Object} s Request object.
  565. *
  566. * @returns {Object} Data modified with post_ID attached.
  567. */
  568. addBefore: function( s ) {
  569. s.data += '&post_id=' + $('#post_ID').val();
  570. return s;
  571. },
  572. /**
  573. * Show the listing of custom fields after fetching.
  574. *
  575. * @ignore
  576. */
  577. addAfter: function() {
  578. $('table#list-table').show();
  579. }
  580. });
  581. }
  582. /*
  583. * Publish Post box (#submitdiv)
  584. */
  585. if ( $('#submitdiv').length ) {
  586. stamp = $('#timestamp').html();
  587. visibility = $('#post-visibility-display').html();
  588. /**
  589. * When the visibility of a post changes sub-options should be shown or hidden.
  590. *
  591. * @ignore
  592. *
  593. * @returns void
  594. */
  595. updateVisibility = function() {
  596. // Show sticky for public posts.
  597. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'public' ) {
  598. $('#sticky').prop('checked', false);
  599. $('#sticky-span').hide();
  600. } else {
  601. $('#sticky-span').show();
  602. }
  603. // Show password input field for password protected post.
  604. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'password' ) {
  605. $('#password-span').hide();
  606. } else {
  607. $('#password-span').show();
  608. }
  609. };
  610. /**
  611. * Make sure all labels represent the current settings.
  612. *
  613. * @ignore
  614. *
  615. * @returns {boolean} False when an invalid timestamp has been selected, otherwise True.
  616. */
  617. updateText = function() {
  618. if ( ! $timestampdiv.length )
  619. return true;
  620. var attemptedDate, originalDate, currentDate, publishOn, postStatus = $('#post_status'),
  621. optPublish = $('option[value="publish"]', postStatus), aa = $('#aa').val(),
  622. mm = $('#mm').val(), jj = $('#jj').val(), hh = $('#hh').val(), mn = $('#mn').val();
  623. attemptedDate = new Date( aa, mm - 1, jj, hh, mn );
  624. originalDate = new Date( $('#hidden_aa').val(), $('#hidden_mm').val() -1, $('#hidden_jj').val(), $('#hidden_hh').val(), $('#hidden_mn').val() );
  625. currentDate = new Date( $('#cur_aa').val(), $('#cur_mm').val() -1, $('#cur_jj').val(), $('#cur_hh').val(), $('#cur_mn').val() );
  626. // Catch unexpected date problems.
  627. if ( attemptedDate.getFullYear() != aa || (1 + attemptedDate.getMonth()) != mm || attemptedDate.getDate() != jj || attemptedDate.getMinutes() != mn ) {
  628. $timestampdiv.find('.timestamp-wrap').addClass('form-invalid');
  629. return false;
  630. } else {
  631. $timestampdiv.find('.timestamp-wrap').removeClass('form-invalid');
  632. }
  633. // Determine what the publish should be depending on the date and post status.
  634. if ( attemptedDate > currentDate && $('#original_post_status').val() != 'future' ) {
  635. publishOn = postL10n.publishOnFuture;
  636. $('#publish').val( postL10n.schedule );
  637. } else if ( attemptedDate <= currentDate && $('#original_post_status').val() != 'publish' ) {
  638. publishOn = postL10n.publishOn;
  639. $('#publish').val( postL10n.publish );
  640. } else {
  641. publishOn = postL10n.publishOnPast;
  642. $('#publish').val( postL10n.update );
  643. }
  644. // If the date is the same, set it to trigger update events.
  645. if ( originalDate.toUTCString() == attemptedDate.toUTCString() ) {
  646. // Re-set to the current value.
  647. $('#timestamp').html(stamp);
  648. } else {
  649. $('#timestamp').html(
  650. '\n' + publishOn + ' <b>' +
  651. postL10n.dateFormat
  652. .replace( '%1$s', $( 'option[value="' + mm + '"]', '#mm' ).attr( 'data-text' ) )
  653. .replace( '%2$s', parseInt( jj, 10 ) )
  654. .replace( '%3$s', aa )
  655. .replace( '%4$s', ( '00' + hh ).slice( -2 ) )
  656. .replace( '%5$s', ( '00' + mn ).slice( -2 ) ) +
  657. '</b> '
  658. );
  659. }
  660. // Add "privately published" to post status when applies.
  661. if ( $postVisibilitySelect.find('input:radio:checked').val() == 'private' ) {
  662. $('#publish').val( postL10n.update );
  663. if ( 0 === optPublish.length ) {
  664. postStatus.append('<option value="publish">' + postL10n.privatelyPublished + '</option>');
  665. } else {
  666. optPublish.html( postL10n.privatelyPublished );
  667. }
  668. $('option[value="publish"]', postStatus).prop('selected', true);
  669. $('#misc-publishing-actions .edit-post-status').hide();
  670. } else {
  671. if ( $('#original_post_status').val() == 'future' || $('#original_post_status').val() == 'draft' ) {
  672. if ( optPublish.length ) {
  673. optPublish.remove();
  674. postStatus.val($('#hidden_post_status').val());
  675. }
  676. } else {
  677. optPublish.html( postL10n.published );
  678. }
  679. if ( postStatus.is(':hidden') )
  680. $('#misc-publishing-actions .edit-post-status').show();
  681. }
  682. // Update "Status:" to currently selected status.
  683. $('#post-status-display').text(
  684. wp.sanitize.stripTagsAndEncodeText( $('option:selected', postStatus).text() ) // Remove any potential tags from post status text.
  685. );
  686. // Show or hide the "Save Draft" button.
  687. if ( $('option:selected', postStatus).val() == 'private' || $('option:selected', postStatus).val() == 'publish' ) {
  688. $('#save-post').hide();
  689. } else {
  690. $('#save-post').show();
  691. if ( $('option:selected', postStatus).val() == 'pending' ) {
  692. $('#save-post').show().val( postL10n.savePending );
  693. } else {
  694. $('#save-post').show().val( postL10n.saveDraft );
  695. }
  696. }
  697. return true;
  698. };
  699. // Show the visibility options and hide the toggle button when opened.
  700. $( '#visibility .edit-visibility').click( function( e ) {
  701. e.preventDefault();
  702. if ( $postVisibilitySelect.is(':hidden') ) {
  703. updateVisibility();
  704. $postVisibilitySelect.slideDown( 'fast', function() {
  705. $postVisibilitySelect.find( 'input[type="radio"]' ).first().focus();
  706. } );
  707. $(this).hide();
  708. }
  709. });
  710. // Cancel visibility selection area and hide it from view.
  711. $postVisibilitySelect.find('.cancel-post-visibility').click( function( event ) {
  712. $postVisibilitySelect.slideUp('fast');
  713. $('#visibility-radio-' + $('#hidden-post-visibility').val()).prop('checked', true);
  714. $('#post_password').val($('#hidden-post-password').val());
  715. $('#sticky').prop('checked', $('#hidden-post-sticky').prop('checked'));
  716. $('#post-visibility-display').html(visibility);
  717. $('#visibility .edit-visibility').show().focus();
  718. updateText();
  719. event.preventDefault();
  720. });
  721. // Set the selected visibility as current.
  722. $postVisibilitySelect.find('.save-post-visibility').click( function( event ) { // crazyhorse - multiple ok cancels
  723. $postVisibilitySelect.slideUp('fast');
  724. $('#visibility .edit-visibility').show().focus();
  725. updateText();
  726. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'public' ) {
  727. $('#sticky').prop('checked', false);
  728. }
  729. if ( $('#sticky').prop('checked') ) {
  730. sticky = 'Sticky';
  731. } else {
  732. sticky = '';
  733. }
  734. $('#post-visibility-display').html( postL10n[ $postVisibilitySelect.find('input:radio:checked').val() + sticky ] );
  735. event.preventDefault();
  736. });
  737. // When the selection changes, update labels.
  738. $postVisibilitySelect.find('input:radio').change( function() {
  739. updateVisibility();
  740. });
  741. // Edit publish time click.
  742. $timestampdiv.siblings('a.edit-timestamp').click( function( event ) {
  743. if ( $timestampdiv.is( ':hidden' ) ) {
  744. $timestampdiv.slideDown( 'fast', function() {
  745. $( 'input, select', $timestampdiv.find( '.timestamp-wrap' ) ).first().focus();
  746. } );
  747. $(this).hide();
  748. }
  749. event.preventDefault();
  750. });
  751. // Cancel editing the publish time and hide the settings.
  752. $timestampdiv.find('.cancel-timestamp').click( function( event ) {
  753. $timestampdiv.slideUp('fast').siblings('a.edit-timestamp').show().focus();
  754. $('#mm').val($('#hidden_mm').val());
  755. $('#jj').val($('#hidden_jj').val());
  756. $('#aa').val($('#hidden_aa').val());
  757. $('#hh').val($('#hidden_hh').val());
  758. $('#mn').val($('#hidden_mn').val());
  759. updateText();
  760. event.preventDefault();
  761. });
  762. // Save the changed timestamp.
  763. $timestampdiv.find('.save-timestamp').click( function( event ) { // crazyhorse - multiple ok cancels
  764. if ( updateText() ) {
  765. $timestampdiv.slideUp('fast');
  766. $timestampdiv.siblings('a.edit-timestamp').show().focus();
  767. }
  768. event.preventDefault();
  769. });
  770. // Cancel submit when an invalid timestamp has been selected.
  771. $('#post').on( 'submit', function( event ) {
  772. if ( ! updateText() ) {
  773. event.preventDefault();
  774. $timestampdiv.show();
  775. if ( wp.autosave ) {
  776. wp.autosave.enableButtons();
  777. }
  778. $( '#publishing-action .spinner' ).removeClass( 'is-active' );
  779. }
  780. });
  781. // Post Status edit click.
  782. $postStatusSelect.siblings('a.edit-post-status').click( function( event ) {
  783. if ( $postStatusSelect.is( ':hidden' ) ) {
  784. $postStatusSelect.slideDown( 'fast', function() {
  785. $postStatusSelect.find('select').focus();
  786. } );
  787. $(this).hide();
  788. }
  789. event.preventDefault();
  790. });
  791. // Save the Post Status changes and hide the options.
  792. $postStatusSelect.find('.save-post-status').click( function( event ) {
  793. $postStatusSelect.slideUp( 'fast' ).siblings( 'a.edit-post-status' ).show().focus();
  794. updateText();
  795. event.preventDefault();
  796. });
  797. // Cancel Post Status editing and hide the options.
  798. $postStatusSelect.find('.cancel-post-status').click( function( event ) {
  799. $postStatusSelect.slideUp( 'fast' ).siblings( 'a.edit-post-status' ).show().focus();
  800. $('#post_status').val( $('#hidden_post_status').val() );
  801. updateText();
  802. event.preventDefault();
  803. });
  804. }
  805. /**
  806. * Handle the editing of the post_name. Create the required HTML elements and
  807. * update the changes via AJAX.
  808. *
  809. * @global
  810. *
  811. * @returns void
  812. */
  813. function editPermalink() {
  814. var i, slug_value,
  815. $el, revert_e,
  816. c = 0,
  817. real_slug = $('#post_name'),
  818. revert_slug = real_slug.val(),
  819. permalink = $( '#sample-permalink' ),
  820. permalinkOrig = permalink.html(),
  821. permalinkInner = $( '#sample-permalink a' ).html(),
  822. buttons = $('#edit-slug-buttons'),
  823. buttonsOrig = buttons.html(),
  824. full = $('#editable-post-name-full');
  825. // Deal with Twemoji in the post-name.
  826. full.find( 'img' ).replaceWith( function() { return this.alt; } );
  827. full = full.html();
  828. permalink.html( permalinkInner );
  829. // Save current content to revert to when cancelling.
  830. $el = $( '#editable-post-name' );
  831. revert_e = $el.html();
  832. buttons.html( '<button type="button" class="save button button-small">' + postL10n.ok + '</button> <button type="button" class="cancel button-link">' + postL10n.cancel + '</button>' );
  833. // Save permalink changes.
  834. buttons.children( '.save' ).click( function() {
  835. var new_slug = $el.children( 'input' ).val();
  836. if ( new_slug == $('#editable-post-name-full').text() ) {
  837. buttons.children('.cancel').click();
  838. return;
  839. }
  840. $.post(
  841. ajaxurl,
  842. {
  843. action: 'sample-permalink',
  844. post_id: postId,
  845. new_slug: new_slug,
  846. new_title: $('#title').val(),
  847. samplepermalinknonce: $('#samplepermalinknonce').val()
  848. },
  849. function(data) {
  850. var box = $('#edit-slug-box');
  851. box.html(data);
  852. if (box.hasClass('hidden')) {
  853. box.fadeIn('fast', function () {
  854. box.removeClass('hidden');
  855. });
  856. }
  857. buttons.html(buttonsOrig);
  858. permalink.html(permalinkOrig);
  859. real_slug.val(new_slug);
  860. $( '.edit-slug' ).focus();
  861. wp.a11y.speak( postL10n.permalinkSaved );
  862. }
  863. );
  864. });
  865. // Cancel editing of permalink.
  866. buttons.children( '.cancel' ).click( function() {
  867. $('#view-post-btn').show();
  868. $el.html(revert_e);
  869. buttons.html(buttonsOrig);
  870. permalink.html(permalinkOrig);
  871. real_slug.val(revert_slug);
  872. $( '.edit-slug' ).focus();
  873. });
  874. // If more than 1/4th of 'full' is '%', make it empty.
  875. for ( i = 0; i < full.length; ++i ) {
  876. if ( '%' == full.charAt(i) )
  877. c++;
  878. }
  879. slug_value = ( c > full.length / 4 ) ? '' : full;
  880. $el.html( '<input type="text" id="new-post-slug" value="' + slug_value + '" autocomplete="off" />' ).children( 'input' ).keydown( function( e ) {
  881. var key = e.which;
  882. // On [enter], just save the new slug, don't save the post.
  883. if ( 13 === key ) {
  884. e.preventDefault();
  885. buttons.children( '.save' ).click();
  886. }
  887. // On [esc] cancel the editing.
  888. if ( 27 === key ) {
  889. buttons.children( '.cancel' ).click();
  890. }
  891. } ).keyup( function() {
  892. real_slug.val( this.value );
  893. }).focus();
  894. }
  895. $( '#titlediv' ).on( 'click', '.edit-slug', function() {
  896. editPermalink();
  897. });
  898. /**
  899. * Adds screen reader text to the title label when needed.
  900. *
  901. * Use the 'screen-reader-text' class to emulate a placeholder attribute
  902. * and hide the label when entering a value.
  903. *
  904. * @param {string} id Optional. HTML ID to add the screen reader helper text to.
  905. *
  906. * @global
  907. *
  908. * @returns void
  909. */
  910. window.wptitlehint = function( id ) {
  911. id = id || 'title';
  912. var title = $( '#' + id ), titleprompt = $( '#' + id + '-prompt-text' );
  913. if ( '' === title.val() ) {
  914. titleprompt.removeClass( 'screen-reader-text' );
  915. }
  916. title.on( 'input', function() {
  917. if ( '' === this.value ) {
  918. titleprompt.removeClass( 'screen-reader-text' );
  919. return;
  920. }
  921. titleprompt.addClass( 'screen-reader-text' );
  922. } );
  923. };
  924. wptitlehint();
  925. // Resize the WYSIWYG and plain text editors.
  926. ( function() {
  927. var editor, offset, mce,
  928. $handle = $('#post-status-info'),
  929. $postdivrich = $('#postdivrich');
  930. // If there are no textareas or we are on a touch device, we can't do anything.
  931. if ( ! $textarea.length || 'ontouchstart' in window ) {
  932. // Hide the resize handle.
  933. $('#content-resize-handle').hide();
  934. return;
  935. }
  936. /**
  937. * Handle drag event.
  938. *
  939. * @param {Object} event Event containing details about the drag.
  940. */
  941. function dragging( event ) {
  942. if ( $postdivrich.hasClass( 'wp-editor-expand' ) ) {
  943. return;
  944. }
  945. if ( mce ) {
  946. editor.theme.resizeTo( null, offset + event.pageY );
  947. } else {
  948. $textarea.height( Math.max( 50, offset + event.pageY ) );
  949. }
  950. event.preventDefault();
  951. }
  952. /**
  953. * When the dragging stopped make sure we return focus and do a sanity check on the height.
  954. */
  955. function endDrag() {
  956. var height, toolbarHeight;
  957. if ( $postdivrich.hasClass( 'wp-editor-expand' ) ) {
  958. return;
  959. }
  960. if ( mce ) {
  961. editor.focus();
  962. toolbarHeight = parseInt( $( '#wp-content-editor-container .mce-toolbar-grp' ).height(), 10 );
  963. if ( toolbarHeight < 10 || toolbarHeight > 200 ) {
  964. toolbarHeight = 30;
  965. }
  966. height = parseInt( $('#content_ifr').css('height'), 10 ) + toolbarHeight - 28;
  967. } else {
  968. $textarea.focus();
  969. height = parseInt( $textarea.css('height'), 10 );
  970. }
  971. $document.off( '.wp-editor-resize' );
  972. // Sanity check: normalize height to stay within acceptable ranges.
  973. if ( height && height > 50 && height < 5000 ) {
  974. setUserSetting( 'ed_size', height );
  975. }
  976. }
  977. $handle.on( 'mousedown.wp-editor-resize', function( event ) {
  978. if ( typeof tinymce !== 'undefined' ) {
  979. editor = tinymce.get('content');
  980. }
  981. if ( editor && ! editor.isHidden() ) {
  982. mce = true;
  983. offset = $('#content_ifr').height() - event.pageY;
  984. } else {
  985. mce = false;
  986. offset = $textarea.height() - event.pageY;
  987. $textarea.blur();
  988. }
  989. $document.on( 'mousemove.wp-editor-resize', dragging )
  990. .on( 'mouseup.wp-editor-resize mouseleave.wp-editor-resize', endDrag );
  991. event.preventDefault();
  992. }).on( 'mouseup.wp-editor-resize', endDrag );
  993. })();
  994. // TinyMCE specific handling of Post Format changes to reflect in the editor.
  995. if ( typeof tinymce !== 'undefined' ) {
  996. // When changing post formats, change the editor body class.
  997. $( '#post-formats-select input.post-format' ).on( 'change.set-editor-class', function() {
  998. var editor, body, format = this.id;
  999. if ( format && $( this ).prop( 'checked' ) && ( editor = tinymce.get( 'content' ) ) ) {
  1000. body = editor.getBody();
  1001. body.className = body.className.replace( /\bpost-format-[^ ]+/, '' );
  1002. editor.dom.addClass( body, format == 'post-format-0' ? 'post-format-standard' : format );
  1003. $( document ).trigger( 'editor-classchange' );
  1004. }
  1005. });
  1006. // When changing page template, change the editor body class
  1007. $( '#page_template' ).on( 'change.set-editor-class', function() {
  1008. var editor, body, pageTemplate = $( this ).val() || '';
  1009. pageTemplate = pageTemplate.substr( pageTemplate.lastIndexOf( '/' ) + 1, pageTemplate.length )
  1010. .replace( /\.php$/, '' )
  1011. .replace( /\./g, '-' );
  1012. if ( pageTemplate && ( editor = tinymce.get( 'content' ) ) ) {
  1013. body = editor.getBody();
  1014. body.className = body.className.replace( /\bpage-template-[^ ]+/, '' );
  1015. editor.dom.addClass( body, 'page-template-' + pageTemplate );
  1016. $( document ).trigger( 'editor-classchange' );
  1017. }
  1018. });
  1019. }
  1020. // Save on pressing [ctrl]/[command] + [s] in the Text editor.
  1021. $textarea.on( 'keydown.wp-autosave', function( event ) {
  1022. // Key [s] has code 83.
  1023. if ( event.which === 83 ) {
  1024. if ( event.shiftKey || event.altKey || ( isMac && ( ! event.metaKey || event.ctrlKey ) ) || ( ! isMac && ! event.ctrlKey ) ) {
  1025. return;
  1026. }
  1027. wp.autosave && wp.autosave.server.triggerSave();
  1028. event.preventDefault();
  1029. }
  1030. });
  1031. // If the last status was auto-draft and the save is triggered, edit the current URL.
  1032. if ( $( '#original_post_status' ).val() === 'auto-draft' && window.history.replaceState ) {
  1033. var location;
  1034. $( '#publish' ).on( 'click', function() {
  1035. location = window.location.href;
  1036. location += ( location.indexOf( '?' ) !== -1 ) ? '&' : '?';
  1037. location += 'wp-post-new-reload=true';
  1038. window.history.replaceState( null, null, location );
  1039. });
  1040. }
  1041. });
  1042. /**
  1043. * TinyMCE word count display
  1044. */
  1045. ( function( $, counter ) {
  1046. $( function() {
  1047. var $content = $( '#content' ),
  1048. $count = $( '#wp-word-count' ).find( '.word-count' ),
  1049. prevCount = 0,
  1050. contentEditor;
  1051. /**
  1052. * Get the word count from TinyMCE and display it
  1053. */
  1054. function update() {
  1055. var text, count;
  1056. if ( ! contentEditor || contentEditor.isHidden() ) {
  1057. text = $content.val();
  1058. } else {
  1059. text = contentEditor.getContent( { format: 'raw' } );
  1060. }
  1061. count = counter.count( text );
  1062. if ( count !== prevCount ) {
  1063. $count.text( count );
  1064. }
  1065. prevCount = count;
  1066. }
  1067. /**
  1068. * Bind the word count update triggers.
  1069. *
  1070. * When a node change in the main TinyMCE editor has been triggered.
  1071. * When a key has been released in the plain text content editor.
  1072. */
  1073. $( document ).on( 'tinymce-editor-init', function( event, editor ) {
  1074. if ( editor.id !== 'content' ) {
  1075. return;
  1076. }
  1077. contentEditor = editor;
  1078. editor.on( 'nodechange keyup', _.debounce( update, 1000 ) );
  1079. } );
  1080. $content.on( 'input keyup', _.debounce( update, 1000 ) );
  1081. update();
  1082. } );
  1083. } )( jQuery, new wp.utils.WordCounter() );