���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC PK1YZ'8 media.min.jsnu[/*! This file is auto-generated */ !function(t){window.findPosts={open:function(n,e){var i=t(".ui-find-overlay");return 0===i.length&&(t("body").append('
'),findPosts.overlay()),i.show(),n&&e&&t("#affected").attr("name",n).val(e),t("#find-posts").show(),t("#find-posts-input").trigger("focus").on("keyup",function(n){27==n.which&&findPosts.close()}),findPosts.send(),!1},close:function(){t("#find-posts-response").empty(),t("#find-posts").hide(),t(".ui-find-overlay").hide()},overlay:function(){t(".ui-find-overlay").on("click",function(){findPosts.close()})},send:function(){var n={ps:t("#find-posts-input").val(),action:"find_posts",_ajax_nonce:t("#_ajax_nonce").val()},e=t(".find-box-search .spinner");e.addClass("is-active"),t.ajax(ajaxurl,{type:"POST",data:n,dataType:"json"}).always(function(){e.removeClass("is-active")}).done(function(n){n.success||t("#find-posts-response").text(wp.i18n.__("An error has occurred. Please reload the page and try again.")),t("#find-posts-response").html(n.data)}).fail(function(){t("#find-posts-response").text(wp.i18n.__("An error has occurred. Please reload the page and try again."))})}},t(function(){var o,n,e=t("#wp-media-grid"),i=new ClipboardJS(".copy-attachment-url.media-library"),s=null;e.length&&window.wp&&window.wp.media&&(n=_wpMediaGridSettings,n=window.wp.media({frame:"manage",container:e,library:n.queryVars}).open(),e.trigger("wp-media-grid-ready",n)),t("#find-posts-submit").on("click",function(n){t('#find-posts-response input[type="radio"]:checked').length||n.preventDefault()}),t("#find-posts .find-box-search :input").on("keypress",function(n){if(13==n.which)return findPosts.send(),!1}),t("#find-posts-search").on("click",findPosts.send),t("#find-posts-close").on("click",findPosts.close),t("#doaction").on("click",function(e){t('select[name="action"]').each(function(){var n=t(this).val();"attach"===n?(e.preventDefault(),findPosts.open()):"delete"!==n||showNotice.warn()||e.preventDefault()})}),t(".find-box-inside").on("click","tr",function(){t(this).find(".found-radio input").prop("checked",!0)}),i.on("success",function(n){var e=t(n.trigger),i=t(".success",e.closest(".copy-to-clipboard-container"));n.clearSelection(),s&&s.addClass("hidden"),clearTimeout(o),i.removeClass("hidden"),o=setTimeout(function(){i.addClass("hidden"),s=null},3e3),s=i,wp.a11y.speak(wp.i18n.__("The file URL has been copied to your clipboard"))})})}(jQuery);PK1YZ=\Fpost.jsnu[/** * @file Contains all dynamic functionality needed on post and term pages. * * @output wp-admin/js/post.js */ /* global ajaxurl, wpAjax, postboxes, pagenow, tinymce, alert, deleteUserSetting, ClipboardJS */ /* global theList:true, theExtraList:true, getUserSetting, setUserSetting, commentReply, commentsBox */ /* global WPSetThumbnailHTML, wptitlehint */ // Backward compatibility: prevent fatal errors. window.makeSlugeditClickable = window.editPermalink = function(){}; // Make sure the wp object exists. window.wp = window.wp || {}; ( function( $ ) { var titleHasFocus = false, __ = wp.i18n.__; /** * Control loading of comments on the post and term edit pages. * * @type {{st: number, get: commentsBox.get, load: commentsBox.load}} * * @namespace commentsBox */ window.commentsBox = { // Comment offset to use when fetching new comments. st : 0, /** * Fetch comments using Ajax and display them in the box. * * @memberof commentsBox * * @param {number} total Total number of comments for this post. * @param {number} num Optional. Number of comments to fetch, defaults to 20. * @return {boolean} Always returns false. */ get : function(total, num) { var st = this.st, data; if ( ! num ) num = 20; this.st += num; this.total = total; $( '#commentsdiv .spinner' ).addClass( 'is-active' ); data = { 'action' : 'get-comments', 'mode' : 'single', '_ajax_nonce' : $('#add_comment_nonce').val(), 'p' : $('#post_ID').val(), 'start' : st, 'number' : num }; $.post( ajaxurl, data, function(r) { r = wpAjax.parseAjaxResponse(r); $('#commentsdiv .widefat').show(); $( '#commentsdiv .spinner' ).removeClass( 'is-active' ); if ( 'object' == typeof r && r.responses[0] ) { $('#the-comment-list').append( r.responses[0].data ); theList = theExtraList = null; $( 'a[className*=\':\']' ).off(); // If the offset is over the total number of comments we cannot fetch any more, so hide the button. if ( commentsBox.st > commentsBox.total ) $('#show-comments').hide(); else $('#show-comments').show().children('a').text( __( 'Show more comments' ) ); return; } else if ( 1 == r ) { $('#show-comments').text( __( 'No more comments found.' ) ); return; } $('#the-comment-list').append(''+wpAjax.broken+''); } ); return false; }, /** * Load the next batch of comments. * * @memberof commentsBox * * @param {number} total Total number of comments to load. */ load: function(total){ this.st = jQuery('#the-comment-list tr.comment:visible').length; this.get(total); } }; /** * Overwrite the content of the Featured Image postbox * * @param {string} html New HTML to be displayed in the content area of the postbox. * * @global */ window.WPSetThumbnailHTML = function(html){ $('.inside', '#postimagediv').html(html); }; /** * Set the Image ID of the Featured Image * * @param {number} id The post_id of the image to use as Featured Image. * * @global */ window.WPSetThumbnailID = function(id){ var field = $('input[value="_thumbnail_id"]', '#list-table'); if ( field.length > 0 ) { $('#meta\\[' + field.attr('id').match(/[0-9]+/) + '\\]\\[value\\]').text(id); } }; /** * Remove the Featured Image * * @param {string} nonce Nonce to use in the request. * * @global */ window.WPRemoveThumbnail = function(nonce){ $.post( ajaxurl, { action: 'set-post-thumbnail', post_id: $( '#post_ID' ).val(), thumbnail_id: -1, _ajax_nonce: nonce, cookie: encodeURIComponent( document.cookie ) }, /** * Handle server response * * @param {string} str Response, will be '0' when an error occurred otherwise contains link to add Featured Image. */ function(str){ if ( str == '0' ) { alert( __( 'Could not set that as the thumbnail image. Try a different attachment.' ) ); } else { WPSetThumbnailHTML(str); } } ); }; /** * Heartbeat locks. * * Used to lock editing of an object by only one user at a time. * * When the user does not send a heartbeat in a heartbeat-time * the user is no longer editing and another user can start editing. */ $(document).on( 'heartbeat-send.refresh-lock', function( e, data ) { var lock = $('#active_post_lock').val(), post_id = $('#post_ID').val(), send = {}; if ( ! post_id || ! $('#post-lock-dialog').length ) return; send.post_id = post_id; if ( lock ) send.lock = lock; data['wp-refresh-post-lock'] = send; }).on( 'heartbeat-tick.refresh-lock', function( e, data ) { // Post locks: update the lock string or show the dialog if somebody has taken over editing. var received, wrap, avatar; if ( data['wp-refresh-post-lock'] ) { received = data['wp-refresh-post-lock']; if ( received.lock_error ) { // Show "editing taken over" message. wrap = $('#post-lock-dialog'); if ( wrap.length && ! wrap.is(':visible') ) { if ( wp.autosave ) { // Save the latest changes and disable. $(document).one( 'heartbeat-tick', function() { wp.autosave.server.suspend(); wrap.removeClass('saving').addClass('saved'); $(window).off( 'beforeunload.edit-post' ); }); wrap.addClass('saving'); wp.autosave.server.triggerSave(); } if ( received.lock_error.avatar_src ) { avatar = $( '', { 'class': 'avatar avatar-64 photo', width: 64, height: 64, alt: '', src: received.lock_error.avatar_src, srcset: received.lock_error.avatar_src_2x ? received.lock_error.avatar_src_2x + ' 2x' : undefined } ); wrap.find('div.post-locked-avatar').empty().append( avatar ); } wrap.show().find('.currently-editing').text( received.lock_error.text ); wrap.find('.wp-tab-first').trigger( 'focus' ); } } else if ( received.new_lock ) { $('#active_post_lock').val( received.new_lock ); } } }).on( 'before-autosave.update-post-slug', function() { titleHasFocus = document.activeElement && document.activeElement.id === 'title'; }).on( 'after-autosave.update-post-slug', function() { /* * Create slug area only if not already there * and the title field was not focused (user was not typing a title) when autosave ran. */ if ( ! $('#edit-slug-box > *').length && ! titleHasFocus ) { $.post( ajaxurl, { action: 'sample-permalink', post_id: $('#post_ID').val(), new_title: $('#title').val(), samplepermalinknonce: $('#samplepermalinknonce').val() }, function( data ) { if ( data != '-1' ) { $('#edit-slug-box').html(data); } } ); } }); }(jQuery)); /** * Heartbeat refresh nonces. */ (function($) { var check, timeout; /** * Only allow to check for nonce refresh every 30 seconds. */ function schedule() { check = false; window.clearTimeout( timeout ); timeout = window.setTimeout( function(){ check = true; }, 300000 ); } $( function() { schedule(); }).on( 'heartbeat-send.wp-refresh-nonces', function( e, data ) { var post_id, $authCheck = $('#wp-auth-check-wrap'); if ( check || ( $authCheck.length && ! $authCheck.hasClass( 'hidden' ) ) ) { if ( ( post_id = $('#post_ID').val() ) && $('#_wpnonce').val() ) { data['wp-refresh-post-nonces'] = { post_id: post_id }; } } }).on( 'heartbeat-tick.wp-refresh-nonces', function( e, data ) { var nonces = data['wp-refresh-post-nonces']; if ( nonces ) { schedule(); if ( nonces.replace ) { $.each( nonces.replace, function( selector, value ) { $( '#' + selector ).val( value ); }); } if ( nonces.heartbeatNonce ) window.heartbeatSettings.nonce = nonces.heartbeatNonce; } }); }(jQuery)); /** * All post and postbox controls and functionality. */ jQuery( function($) { var stamp, visibility, $submitButtons, updateVisibility, updateText, $textarea = $('#content'), $document = $(document), postId = $('#post_ID').val() || 0, $submitpost = $('#submitpost'), releaseLock = true, $postVisibilitySelect = $('#post-visibility-select'), $timestampdiv = $('#timestampdiv'), $postStatusSelect = $('#post-status-select'), isMac = window.navigator.platform ? window.navigator.platform.indexOf( 'Mac' ) !== -1 : false, copyAttachmentURLClipboard = new ClipboardJS( '.copy-attachment-url.edit-media' ), copyAttachmentURLSuccessTimeout, __ = wp.i18n.__, _x = wp.i18n._x; postboxes.add_postbox_toggles(pagenow); /* * Clear the window name. Otherwise if this is a former preview window where the user navigated to edit another post, * and the first post is still being edited, clicking Preview there will use this window to show the preview. */ window.name = ''; // Post locks: contain focus inside the dialog. If the dialog is shown, focus the first item. $('#post-lock-dialog .notification-dialog').on( 'keydown', function(e) { // Don't do anything when [Tab] is pressed. if ( e.which != 9 ) return; var target = $(e.target); // [Shift] + [Tab] on first tab cycles back to last tab. if ( target.hasClass('wp-tab-first') && e.shiftKey ) { $(this).find('.wp-tab-last').trigger( 'focus' ); e.preventDefault(); // [Tab] on last tab cycles back to first tab. } else if ( target.hasClass('wp-tab-last') && ! e.shiftKey ) { $(this).find('.wp-tab-first').trigger( 'focus' ); e.preventDefault(); } }).filter(':visible').find('.wp-tab-first').trigger( 'focus' ); // Set the heartbeat interval to 10 seconds if post lock dialogs are enabled. if ( wp.heartbeat && $('#post-lock-dialog').length ) { wp.heartbeat.interval( 10 ); } // The form is being submitted by the user. $submitButtons = $submitpost.find( ':submit, a.submitdelete, #post-preview' ).on( 'click.edit-post', function( event ) { var $button = $(this); if ( $button.hasClass('disabled') ) { event.preventDefault(); return; } if ( $button.hasClass('submitdelete') || $button.is( '#post-preview' ) ) { return; } // The form submission can be blocked from JS or by using HTML 5.0 validation on some fields. // Run this only on an actual 'submit'. $('form#post').off( 'submit.edit-post' ).on( 'submit.edit-post', function( event ) { if ( event.isDefaultPrevented() ) { return; } // Stop auto save. if ( wp.autosave ) { wp.autosave.server.suspend(); } if ( typeof commentReply !== 'undefined' ) { /* * Warn the user they have an unsaved comment before submitting * the post data for update. */ if ( ! commentReply.discardCommentChanges() ) { return false; } /* * Close the comment edit/reply form if open to stop the form * action from interfering with the post's form action. */ commentReply.close(); } releaseLock = false; $(window).off( 'beforeunload.edit-post' ); $submitButtons.addClass( 'disabled' ); if ( $button.attr('id') === 'publish' ) { $submitpost.find( '#major-publishing-actions .spinner' ).addClass( 'is-active' ); } else { $submitpost.find( '#minor-publishing .spinner' ).addClass( 'is-active' ); } }); }); // Submit the form saving a draft or an autosave, and show a preview in a new tab. $('#post-preview').on( 'click.post-preview', function( event ) { var $this = $(this), $form = $('form#post'), $previewField = $('input#wp-preview'), target = $this.attr('target') || 'wp-preview', ua = navigator.userAgent.toLowerCase(); event.preventDefault(); if ( $this.hasClass('disabled') ) { return; } if ( wp.autosave ) { wp.autosave.server.tempBlockSave(); } $previewField.val('dopreview'); $form.attr( 'target', target ).trigger( 'submit' ).attr( 'target', '' ); // Workaround for WebKit bug preventing a form submitting twice to the same action. // https://bugs.webkit.org/show_bug.cgi?id=28633 if ( ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1 ) { $form.attr( 'action', function( index, value ) { return value + '?t=' + ( new Date() ).getTime(); }); } $previewField.val(''); }); // Auto save new posts after a title is typed. if ( $( '#auto_draft' ).val() ) { $( '#title' ).on( 'blur', function() { var cancel; if ( ! this.value || $('#edit-slug-box > *').length ) { return; } // Cancel the auto save when the blur was triggered by the user submitting the form. $('form#post').one( 'submit', function() { cancel = true; }); window.setTimeout( function() { if ( ! cancel && wp.autosave ) { wp.autosave.server.triggerSave(); } }, 200 ); }); } $document.on( 'autosave-disable-buttons.edit-post', function() { $submitButtons.addClass( 'disabled' ); }).on( 'autosave-enable-buttons.edit-post', function() { if ( ! wp.heartbeat || ! wp.heartbeat.hasConnectionError() ) { $submitButtons.removeClass( 'disabled' ); } }).on( 'before-autosave.edit-post', function() { $( '.autosave-message' ).text( __( 'Saving Draft…' ) ); }).on( 'after-autosave.edit-post', function( event, data ) { $( '.autosave-message' ).text( data.message ); if ( $( document.body ).hasClass( 'post-new-php' ) ) { $( '.submitbox .submitdelete' ).show(); } }); /* * When the user is trying to load another page, or reloads current page * show a confirmation dialog when there are unsaved changes. */ $( window ).on( 'beforeunload.edit-post', function( event ) { var editor = window.tinymce && window.tinymce.get( 'content' ); var changed = false; if ( wp.autosave ) { changed = wp.autosave.server.postChanged(); } else if ( editor ) { changed = ( ! editor.isHidden() && editor.isDirty() ); } if ( changed ) { event.preventDefault(); // The return string is needed for browser compat. // See https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event. return __( 'The changes you made will be lost if you navigate away from this page.' ); } }).on( 'pagehide.edit-post', function( event ) { if ( ! releaseLock ) { return; } /* * Unload is triggered (by hand) on removing the Thickbox iframe. * Make sure we process only the main document unload. */ if ( event.target && event.target.nodeName != '#document' ) { return; } var postID = $('#post_ID').val(); var postLock = $('#active_post_lock').val(); if ( ! postID || ! postLock ) { return; } var data = { action: 'wp-remove-post-lock', _wpnonce: $('#_wpnonce').val(), post_ID: postID, active_post_lock: postLock }; if ( window.FormData && window.navigator.sendBeacon ) { var formData = new window.FormData(); $.each( data, function( key, value ) { formData.append( key, value ); }); if ( window.navigator.sendBeacon( ajaxurl, formData ) ) { return; } } // Fall back to a synchronous POST request. // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon $.post({ async: false, data: data, url: ajaxurl }); }); // Multiple taxonomies. if ( $('#tagsdiv-post_tag').length ) { window.tagBox && window.tagBox.init(); } else { $('.meta-box-sortables').children('div.postbox').each(function(){ if ( this.id.indexOf('tagsdiv-') === 0 ) { window.tagBox && window.tagBox.init(); return false; } }); } // Handle categories. $('.categorydiv').each( function(){ var this_id = $(this).attr('id'), catAddBefore, catAddAfter, taxonomyParts, taxonomy, settingName; taxonomyParts = this_id.split('-'); taxonomyParts.shift(); taxonomy = taxonomyParts.join('-'); settingName = taxonomy + '_tab'; if ( taxonomy == 'category' ) { settingName = 'cats'; } // @todo Move to jQuery 1.3+, support for multiple hierarchical taxonomies, see wp-lists.js. $('a', '#' + taxonomy + '-tabs').on( 'click', function( e ) { e.preventDefault(); var t = $(this).attr('href'); $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); $('#' + taxonomy + '-tabs').siblings('.tabs-panel').hide(); $(t).show(); if ( '#' + taxonomy + '-all' == t ) { deleteUserSetting( settingName ); } else { setUserSetting( settingName, 'pop' ); } }); if ( getUserSetting( settingName ) ) $('a[href="#' + taxonomy + '-pop"]', '#' + taxonomy + '-tabs').trigger( 'click' ); // Add category button controls. $('#new' + taxonomy).one( 'focus', function() { $( this ).val( '' ).removeClass( 'form-input-tip' ); }); // On [Enter] submit the taxonomy. $('#new' + taxonomy).on( 'keypress', function(event){ if( 13 === event.keyCode ) { event.preventDefault(); $('#' + taxonomy + '-add-submit').trigger( 'click' ); } }); // After submitting a new taxonomy, re-focus the input field. $('#' + taxonomy + '-add-submit').on( 'click', function() { $('#new' + taxonomy).trigger( 'focus' ); }); /** * Before adding a new taxonomy, disable submit button. * * @param {Object} s Taxonomy object which will be added. * * @return {Object} */ catAddBefore = function( s ) { if ( !$('#new'+taxonomy).val() ) { return false; } s.data += '&' + $( ':checked', '#'+taxonomy+'checklist' ).serialize(); $( '#' + taxonomy + '-add-submit' ).prop( 'disabled', true ); return s; }; /** * Re-enable submit button after a taxonomy has been added. * * Re-enable submit button. * If the taxonomy has a parent place the taxonomy underneath the parent. * * @param {Object} r Response. * @param {Object} s Taxonomy data. * * @return {void} */ catAddAfter = function( r, s ) { var sup, drop = $('#new'+taxonomy+'_parent'); $( '#' + taxonomy + '-add-submit' ).prop( 'disabled', false ); if ( 'undefined' != s.parsed.responses[0] && (sup = s.parsed.responses[0].supplemental.newcat_parent) ) { drop.before(sup); drop.remove(); } }; $('#' + taxonomy + 'checklist').wpList({ alt: '', response: taxonomy + '-ajax-response', addBefore: catAddBefore, addAfter: catAddAfter }); // Add new taxonomy button toggles input form visibility. $('#' + taxonomy + '-add-toggle').on( 'click', function( e ) { e.preventDefault(); $('#' + taxonomy + '-adder').toggleClass( 'wp-hidden-children' ); $('a[href="#' + taxonomy + '-all"]', '#' + taxonomy + '-tabs').trigger( 'click' ); $('#new'+taxonomy).trigger( 'focus' ); }); // Sync checked items between "All {taxonomy}" and "Most used" lists. $('#' + taxonomy + 'checklist, #' + taxonomy + 'checklist-pop').on( 'click', 'li.popular-category > label input[type="checkbox"]', function() { var t = $(this), c = t.is(':checked'), id = t.val(); if ( id && t.parents('#taxonomy-'+taxonomy).length ) { $('input#in-' + taxonomy + '-' + id + ', input[id^="in-' + taxonomy + '-' + id + '-"]').prop('checked', c); $('input#in-popular-' + taxonomy + '-' + id).prop('checked', c); } } ); }); // End cats. // Custom Fields postbox. if ( $('#postcustom').length ) { $( '#the-list' ).wpList( { /** * Add current post_ID to request to fetch custom fields * * @ignore * * @param {Object} s Request object. * * @return {Object} Data modified with post_ID attached. */ addBefore: function( s ) { s.data += '&post_id=' + $('#post_ID').val(); return s; }, /** * Show the listing of custom fields after fetching. * * @ignore */ addAfter: function() { $('table#list-table').show(); } }); } /* * Publish Post box (#submitdiv) */ if ( $('#submitdiv').length ) { stamp = $('#timestamp').html(); visibility = $('#post-visibility-display').html(); /** * When the visibility of a post changes sub-options should be shown or hidden. * * @ignore * * @return {void} */ updateVisibility = function() { // Show sticky for public posts. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'public' ) { $('#sticky').prop('checked', false); $('#sticky-span').hide(); } else { $('#sticky-span').show(); } // Show password input field for password protected post. if ( $postVisibilitySelect.find('input:radio:checked').val() != 'password' ) { $('#password-span').hide(); } else { $('#password-span').show(); } }; /** * Make sure all labels represent the current settings. * * @ignore * * @return {boolean} False when an invalid timestamp has been selected, otherwise True. */ updateText = function() { if ( ! $timestampdiv.length ) return true; var attemptedDate, originalDate, currentDate, publishOn, postStatus = $('#post_status'), optPublish = $('option[value="publish"]', postStatus), aa = $('#aa').val(), mm = $('#mm').val(), jj = $('#jj').val(), hh = $('#hh').val(), mn = $('#mn').val(); attemptedDate = new Date( aa, mm - 1, jj, hh, mn ); originalDate = new Date( $('#hidden_aa').val(), $('#hidden_mm').val() -1, $('#hidden_jj').val(), $('#hidden_hh').val(), $('#hidden_mn').val() ); currentDate = new Date( $('#cur_aa').val(), $('#cur_mm').val() -1, $('#cur_jj').val(), $('#cur_hh').val(), $('#cur_mn').val() ); // Catch unexpected date problems. if ( attemptedDate.getFullYear() != aa || (1 + attemptedDate.getMonth()) != mm || attemptedDate.getDate() != jj || attemptedDate.getMinutes() != mn ) { $timestampdiv.find('.timestamp-wrap').addClass('form-invalid'); return false; } else { $timestampdiv.find('.timestamp-wrap').removeClass('form-invalid'); } // Determine what the publish should be depending on the date and post status. if ( attemptedDate > currentDate ) { publishOn = __( 'Schedule for:' ); $('#publish').val( _x( 'Schedule', 'post action/button label' ) ); } else if ( attemptedDate <= currentDate && $('#original_post_status').val() != 'publish' ) { publishOn = __( 'Publish on:' ); $('#publish').val( __( 'Publish' ) ); } else { publishOn = __( 'Published on:' ); $('#publish').val( __( 'Update' ) ); } // If the date is the same, set it to trigger update events. if ( originalDate.toUTCString() == attemptedDate.toUTCString() ) { // Re-set to the current value. $('#timestamp').html(stamp); } else { $('#timestamp').html( '\n' + publishOn + ' ' + // translators: 1: Month, 2: Day, 3: Year, 4: Hour, 5: Minute. __( '%1$s %2$s, %3$s at %4$s:%5$s' ) .replace( '%1$s', $( 'option[value="' + mm + '"]', '#mm' ).attr( 'data-text' ) ) .replace( '%2$s', parseInt( jj, 10 ) ) .replace( '%3$s', aa ) .replace( '%4$s', ( '00' + hh ).slice( -2 ) ) .replace( '%5$s', ( '00' + mn ).slice( -2 ) ) + ' ' ); } // Add "privately published" to post status when applies. if ( $postVisibilitySelect.find('input:radio:checked').val() == 'private' ) { $('#publish').val( __( 'Update' ) ); if ( 0 === optPublish.length ) { postStatus.append(''); } else { optPublish.html( __( 'Privately Published' ) ); } $('option[value="publish"]', postStatus).prop('selected', true); $('#misc-publishing-actions .edit-post-status').hide(); } else { if ( $('#original_post_status').val() == 'future' || $('#original_post_status').val() == 'draft' ) { if ( optPublish.length ) { optPublish.remove(); postStatus.val($('#hidden_post_status').val()); } } else { optPublish.html( __( 'Published' ) ); } if ( postStatus.is(':hidden') ) $('#misc-publishing-actions .edit-post-status').show(); } // Update "Status:" to currently selected status. $('#post-status-display').text( // Remove any potential tags from post status text. wp.sanitize.stripTagsAndEncodeText( $('option:selected', postStatus).text() ) ); // Show or hide the "Save Draft" button. if ( $('option:selected', postStatus).val() == 'private' || $('option:selected', postStatus).val() == 'publish' ) { $('#save-post').hide(); } else { $('#save-post').show(); if ( $('option:selected', postStatus).val() == 'pending' ) { $('#save-post').show().val( __( 'Save as Pending' ) ); } else { $('#save-post').show().val( __( 'Save Draft' ) ); } } return true; }; // Show the visibility options and hide the toggle button when opened. $( '#visibility .edit-visibility').on( 'click', function( e ) { e.preventDefault(); if ( $postVisibilitySelect.is(':hidden') ) { updateVisibility(); $postVisibilitySelect.slideDown( 'fast', function() { $postVisibilitySelect.find( 'input[type="radio"]' ).first().trigger( 'focus' ); } ); $(this).hide(); } }); // Cancel visibility selection area and hide it from view. $postVisibilitySelect.find('.cancel-post-visibility').on( 'click', function( event ) { $postVisibilitySelect.slideUp('fast'); $('#visibility-radio-' + $('#hidden-post-visibility').val()).prop('checked', true); $('#post_password').val($('#hidden-post-password').val()); $('#sticky').prop('checked', $('#hidden-post-sticky').prop('checked')); $('#post-visibility-display').html(visibility); $('#visibility .edit-visibility').show().trigger( 'focus' ); updateText(); event.preventDefault(); }); // Set the selected visibility as current. $postVisibilitySelect.find('.save-post-visibility').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. var visibilityLabel = '', selectedVisibility = $postVisibilitySelect.find('input:radio:checked').val(); $postVisibilitySelect.slideUp('fast'); $('#visibility .edit-visibility').show().trigger( 'focus' ); updateText(); if ( 'public' !== selectedVisibility ) { $('#sticky').prop('checked', false); } switch ( selectedVisibility ) { case 'public': visibilityLabel = $( '#sticky' ).prop( 'checked' ) ? __( 'Public, Sticky' ) : __( 'Public' ); break; case 'private': visibilityLabel = __( 'Private' ); break; case 'password': visibilityLabel = __( 'Password Protected' ); break; } $('#post-visibility-display').text( visibilityLabel ); event.preventDefault(); }); // When the selection changes, update labels. $postVisibilitySelect.find('input:radio').on( 'change', function() { updateVisibility(); }); // Edit publish time click. $timestampdiv.siblings('a.edit-timestamp').on( 'click', function( event ) { if ( $timestampdiv.is( ':hidden' ) ) { $timestampdiv.slideDown( 'fast', function() { $( 'input, select', $timestampdiv.find( '.timestamp-wrap' ) ).first().trigger( 'focus' ); } ); $(this).hide(); } event.preventDefault(); }); // Cancel editing the publish time and hide the settings. $timestampdiv.find('.cancel-timestamp').on( 'click', function( event ) { $timestampdiv.slideUp('fast').siblings('a.edit-timestamp').show().trigger( 'focus' ); $('#mm').val($('#hidden_mm').val()); $('#jj').val($('#hidden_jj').val()); $('#aa').val($('#hidden_aa').val()); $('#hh').val($('#hidden_hh').val()); $('#mn').val($('#hidden_mn').val()); updateText(); event.preventDefault(); }); // Save the changed timestamp. $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. if ( updateText() ) { $timestampdiv.slideUp('fast'); $timestampdiv.siblings('a.edit-timestamp').show().trigger( 'focus' ); } event.preventDefault(); }); // Cancel submit when an invalid timestamp has been selected. $('#post').on( 'submit', function( event ) { if ( ! updateText() ) { event.preventDefault(); $timestampdiv.show(); if ( wp.autosave ) { wp.autosave.enableButtons(); } $( '#publishing-action .spinner' ).removeClass( 'is-active' ); } }); // Post Status edit click. $postStatusSelect.siblings('a.edit-post-status').on( 'click', function( event ) { if ( $postStatusSelect.is( ':hidden' ) ) { $postStatusSelect.slideDown( 'fast', function() { $postStatusSelect.find('select').trigger( 'focus' ); } ); $(this).hide(); } event.preventDefault(); }); // Save the Post Status changes and hide the options. $postStatusSelect.find('.save-post-status').on( 'click', function( event ) { $postStatusSelect.slideUp( 'fast' ).siblings( 'a.edit-post-status' ).show().trigger( 'focus' ); updateText(); event.preventDefault(); }); // Cancel Post Status editing and hide the options. $postStatusSelect.find('.cancel-post-status').on( 'click', function( event ) { $postStatusSelect.slideUp( 'fast' ).siblings( 'a.edit-post-status' ).show().trigger( 'focus' ); $('#post_status').val( $('#hidden_post_status').val() ); updateText(); event.preventDefault(); }); } /** * Handle the editing of the post_name. Create the required HTML elements and * update the changes via Ajax. * * @global * * @return {void} */ function editPermalink() { var i, slug_value, slug_label, $el, revert_e, c = 0, real_slug = $('#post_name'), revert_slug = real_slug.val(), permalink = $( '#sample-permalink' ), permalinkOrig = permalink.html(), permalinkInner = $( '#sample-permalink a' ).html(), buttons = $('#edit-slug-buttons'), buttonsOrig = buttons.html(), full = $('#editable-post-name-full'); // Deal with Twemoji in the post-name. full.find( 'img' ).replaceWith( function() { return this.alt; } ); full = full.html(); permalink.html( permalinkInner ); // Save current content to revert to when cancelling. $el = $( '#editable-post-name' ); revert_e = $el.html(); buttons.html( ' ' + '' ); // Save permalink changes. buttons.children( '.save' ).on( 'click', function() { var new_slug = $el.children( 'input' ).val(); if ( new_slug == $('#editable-post-name-full').text() ) { buttons.children('.cancel').trigger( 'click' ); return; } $.post( ajaxurl, { action: 'sample-permalink', post_id: postId, new_slug: new_slug, new_title: $('#title').val(), samplepermalinknonce: $('#samplepermalinknonce').val() }, function(data) { var box = $('#edit-slug-box'); box.html(data); if (box.hasClass('hidden')) { box.fadeIn('fast', function () { box.removeClass('hidden'); }); } buttons.html(buttonsOrig); permalink.html(permalinkOrig); real_slug.val(new_slug); $( '.edit-slug' ).trigger( 'focus' ); wp.a11y.speak( __( 'Permalink saved' ) ); } ); }); // Cancel editing of permalink. buttons.children( '.cancel' ).on( 'click', function() { $('#view-post-btn').show(); $el.html(revert_e); buttons.html(buttonsOrig); permalink.html(permalinkOrig); real_slug.val(revert_slug); $( '.edit-slug' ).trigger( 'focus' ); }); // If more than 1/4th of 'full' is '%', make it empty. for ( i = 0; i < full.length; ++i ) { if ( '%' == full.charAt(i) ) c++; } slug_value = ( c > full.length / 4 ) ? '' : full; slug_label = __( 'URL Slug' ); $el.html( '' + '' ).children( 'input' ).on( 'keydown', function( e ) { var key = e.which; // On [Enter], just save the new slug, don't save the post. if ( 13 === key ) { e.preventDefault(); buttons.children( '.save' ).trigger( 'click' ); } // On [Esc] cancel the editing. if ( 27 === key ) { buttons.children( '.cancel' ).trigger( 'click' ); } } ).on( 'keyup', function() { real_slug.val( this.value ); }).trigger( 'focus' ); } $( '#titlediv' ).on( 'click', '.edit-slug', function() { editPermalink(); }); /** * Adds screen reader text to the title label when needed. * * Use the 'screen-reader-text' class to emulate a placeholder attribute * and hide the label when entering a value. * * @param {string} id Optional. HTML ID to add the screen reader helper text to. * * @global * * @return {void} */ window.wptitlehint = function( id ) { id = id || 'title'; var title = $( '#' + id ), titleprompt = $( '#' + id + '-prompt-text' ); if ( '' === title.val() ) { titleprompt.removeClass( 'screen-reader-text' ); } title.on( 'input', function() { if ( '' === this.value ) { titleprompt.removeClass( 'screen-reader-text' ); return; } titleprompt.addClass( 'screen-reader-text' ); } ); }; wptitlehint(); // Resize the WYSIWYG and plain text editors. ( function() { var editor, offset, mce, $handle = $('#post-status-info'), $postdivrich = $('#postdivrich'); // If there are no textareas or we are on a touch device, we can't do anything. if ( ! $textarea.length || 'ontouchstart' in window ) { // Hide the resize handle. $('#content-resize-handle').hide(); return; } /** * Handle drag event. * * @param {Object} event Event containing details about the drag. */ function dragging( event ) { if ( $postdivrich.hasClass( 'wp-editor-expand' ) ) { return; } if ( mce ) { editor.theme.resizeTo( null, offset + event.pageY ); } else { $textarea.height( Math.max( 50, offset + event.pageY ) ); } event.preventDefault(); } /** * When the dragging stopped make sure we return focus and do a confidence check on the height. */ function endDrag() { var height, toolbarHeight; if ( $postdivrich.hasClass( 'wp-editor-expand' ) ) { return; } if ( mce ) { editor.focus(); toolbarHeight = parseInt( $( '#wp-content-editor-container .mce-toolbar-grp' ).height(), 10 ); if ( toolbarHeight < 10 || toolbarHeight > 200 ) { toolbarHeight = 30; } height = parseInt( $('#content_ifr').css('height'), 10 ) + toolbarHeight - 28; } else { $textarea.trigger( 'focus' ); height = parseInt( $textarea.css('height'), 10 ); } $document.off( '.wp-editor-resize' ); // Confidence check: normalize height to stay within acceptable ranges. if ( height && height > 50 && height < 5000 ) { setUserSetting( 'ed_size', height ); } } $handle.on( 'mousedown.wp-editor-resize', function( event ) { if ( typeof tinymce !== 'undefined' ) { editor = tinymce.get('content'); } if ( editor && ! editor.isHidden() ) { mce = true; offset = $('#content_ifr').height() - event.pageY; } else { mce = false; offset = $textarea.height() - event.pageY; $textarea.trigger( 'blur' ); } $document.on( 'mousemove.wp-editor-resize', dragging ) .on( 'mouseup.wp-editor-resize mouseleave.wp-editor-resize', endDrag ); event.preventDefault(); }).on( 'mouseup.wp-editor-resize', endDrag ); })(); // TinyMCE specific handling of Post Format changes to reflect in the editor. if ( typeof tinymce !== 'undefined' ) { // When changing post formats, change the editor body class. $( '#post-formats-select input.post-format' ).on( 'change.set-editor-class', function() { var editor, body, format = this.id; if ( format && $( this ).prop( 'checked' ) && ( editor = tinymce.get( 'content' ) ) ) { body = editor.getBody(); body.className = body.className.replace( /\bpost-format-[^ ]+/, '' ); editor.dom.addClass( body, format == 'post-format-0' ? 'post-format-standard' : format ); $( document ).trigger( 'editor-classchange' ); } }); // When changing page template, change the editor body class. $( '#page_template' ).on( 'change.set-editor-class', function() { var editor, body, pageTemplate = $( this ).val() || ''; pageTemplate = pageTemplate.substr( pageTemplate.lastIndexOf( '/' ) + 1, pageTemplate.length ) .replace( /\.php$/, '' ) .replace( /\./g, '-' ); if ( pageTemplate && ( editor = tinymce.get( 'content' ) ) ) { body = editor.getBody(); body.className = body.className.replace( /\bpage-template-[^ ]+/, '' ); editor.dom.addClass( body, 'page-template-' + pageTemplate ); $( document ).trigger( 'editor-classchange' ); } }); } // Save on pressing [Ctrl]/[Command] + [S] in the Text editor. $textarea.on( 'keydown.wp-autosave', function( event ) { // Key [S] has code 83. if ( event.which === 83 ) { if ( event.shiftKey || event.altKey || ( isMac && ( ! event.metaKey || event.ctrlKey ) ) || ( ! isMac && ! event.ctrlKey ) ) { return; } wp.autosave && wp.autosave.server.triggerSave(); event.preventDefault(); } }); // If the last status was auto-draft and the save is triggered, edit the current URL. if ( $( '#original_post_status' ).val() === 'auto-draft' && window.history.replaceState ) { var location; $( '#publish' ).on( 'click', function() { location = window.location.href; location += ( location.indexOf( '?' ) !== -1 ) ? '&' : '?'; location += 'wp-post-new-reload=true'; window.history.replaceState( null, null, location ); }); } /** * Copies the attachment URL in the Edit Media page to the clipboard. * * @since 5.5.0 * * @param {MouseEvent} event A click event. * * @return {void} */ copyAttachmentURLClipboard.on( 'success', function( event ) { var triggerElement = $( event.trigger ), successElement = $( '.success', triggerElement.closest( '.copy-to-clipboard-container' ) ); // Clear the selection and move focus back to the trigger. event.clearSelection(); // Show success visual feedback. clearTimeout( copyAttachmentURLSuccessTimeout ); successElement.removeClass( 'hidden' ); // Hide success visual feedback after 3 seconds since last success. copyAttachmentURLSuccessTimeout = setTimeout( function() { successElement.addClass( 'hidden' ); }, 3000 ); // Handle success audible feedback. wp.a11y.speak( __( 'The file URL has been copied to your clipboard' ) ); } ); } ); /** * TinyMCE word count display */ ( function( $, counter ) { $( function() { var $content = $( '#content' ), $count = $( '#wp-word-count' ).find( '.word-count' ), prevCount = 0, contentEditor; /** * Get the word count from TinyMCE and display it */ function update() { var text, count; if ( ! contentEditor || contentEditor.isHidden() ) { text = $content.val(); } else { text = contentEditor.getContent( { format: 'raw' } ); } count = counter.count( text ); if ( count !== prevCount ) { $count.text( count ); } prevCount = count; } /** * Bind the word count update triggers. * * When a node change in the main TinyMCE editor has been triggered. * When a key has been released in the plain text content editor. */ $( document ).on( 'tinymce-editor-init', function( event, editor ) { if ( editor.id !== 'content' ) { return; } contentEditor = editor; editor.on( 'nodechange keyup', _.debounce( update, 1000 ) ); } ); $content.on( 'input keyup', _.debounce( update, 1000 ) ); update(); } ); } )( jQuery, new wp.utils.WordCounter() ); PK1YZ:g g comment.jsnu[/** * @output wp-admin/js/comment.js */ /* global postboxes */ /** * Binds to the document ready event. * * @since 2.5.0 * * @param {jQuery} $ The jQuery object. */ jQuery( function($) { postboxes.add_postbox_toggles('comment'); var $timestampdiv = $('#timestampdiv'), $timestamp = $( '#timestamp' ), stamp = $timestamp.html(), $timestampwrap = $timestampdiv.find( '.timestamp-wrap' ), $edittimestamp = $timestampdiv.siblings( 'a.edit-timestamp' ); /** * Adds event that opens the time stamp form if the form is hidden. * * @listens $edittimestamp:click * * @param {Event} event The event object. * @return {void} */ $edittimestamp.on( 'click', function( event ) { if ( $timestampdiv.is( ':hidden' ) ) { // Slide down the form and set focus on the first field. $timestampdiv.slideDown( 'fast', function() { $( 'input, select', $timestampwrap ).first().trigger( 'focus' ); } ); $(this).hide(); } event.preventDefault(); }); /** * Resets the time stamp values when the cancel button is clicked. * * @listens .cancel-timestamp:click * * @param {Event} event The event object. * @return {void} */ $timestampdiv.find('.cancel-timestamp').on( 'click', function( event ) { // Move focus back to the Edit link. $edittimestamp.show().trigger( 'focus' ); $timestampdiv.slideUp( 'fast' ); $('#mm').val($('#hidden_mm').val()); $('#jj').val($('#hidden_jj').val()); $('#aa').val($('#hidden_aa').val()); $('#hh').val($('#hidden_hh').val()); $('#mn').val($('#hidden_mn').val()); $timestamp.html( stamp ); event.preventDefault(); }); /** * Sets the time stamp values when the ok button is clicked. * * @listens .save-timestamp:click * * @param {Event} event The event object. * @return {void} */ $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. var aa = $('#aa').val(), mm = $('#mm').val(), jj = $('#jj').val(), hh = $('#hh').val(), mn = $('#mn').val(), newD = new Date( aa, mm - 1, jj, hh, mn ); event.preventDefault(); if ( newD.getFullYear() != aa || (1 + newD.getMonth()) != mm || newD.getDate() != jj || newD.getMinutes() != mn ) { $timestampwrap.addClass( 'form-invalid' ); return; } else { $timestampwrap.removeClass( 'form-invalid' ); } $timestamp.html( wp.i18n.__( 'Submitted on:' ) + ' ' + /* translators: 1: Month, 2: Day, 3: Year, 4: Hour, 5: Minute. */ wp.i18n.__( '%1$s %2$s, %3$s at %4$s:%5$s' ) .replace( '%1$s', $( 'option[value="' + mm + '"]', '#mm' ).attr( 'data-text' ) ) .replace( '%2$s', parseInt( jj, 10 ) ) .replace( '%3$s', aa ) .replace( '%4$s', ( '00' + hh ).slice( -2 ) ) .replace( '%5$s', ( '00' + mn ).slice( -2 ) ) + ' ' ); // Move focus back to the Edit link. $edittimestamp.show().trigger( 'focus' ); $timestampdiv.slideUp( 'fast' ); }); }); PK1YZ5k k custom-background.jsnu[/** * @output wp-admin/js/custom-background.js */ /* global ajaxurl */ /** * Registers all events for customizing the background. * * @since 3.0.0 * * @requires jQuery */ (function($) { $( function() { var frame, bgImage = $( '#custom-background-image' ); /** * Instantiates the WordPress color picker and binds the change and clear events. * * @since 3.5.0 * * @return {void} */ $('#background-color').wpColorPicker({ change: function( event, ui ) { bgImage.css('background-color', ui.color.toString()); }, clear: function() { bgImage.css('background-color', ''); } }); /** * Alters the background size CSS property whenever the background size input has changed. * * @since 4.7.0 * * @return {void} */ $( 'select[name="background-size"]' ).on( 'change', function() { bgImage.css( 'background-size', $( this ).val() ); }); /** * Alters the background position CSS property whenever the background position input has changed. * * @since 4.7.0 * * @return {void} */ $( 'input[name="background-position"]' ).on( 'change', function() { bgImage.css( 'background-position', $( this ).val() ); }); /** * Alters the background repeat CSS property whenever the background repeat input has changed. * * @since 3.0.0 * * @return {void} */ $( 'input[name="background-repeat"]' ).on( 'change', function() { bgImage.css( 'background-repeat', $( this ).is( ':checked' ) ? 'repeat' : 'no-repeat' ); }); /** * Alters the background attachment CSS property whenever the background attachment input has changed. * * @since 4.7.0 * * @return {void} */ $( 'input[name="background-attachment"]' ).on( 'change', function() { bgImage.css( 'background-attachment', $( this ).is( ':checked' ) ? 'scroll' : 'fixed' ); }); /** * Binds the event for opening the WP Media dialog. * * @since 3.5.0 * * @return {void} */ $('#choose-from-library-link').on( 'click', function( event ) { var $el = $(this); event.preventDefault(); // If the media frame already exists, reopen it. if ( frame ) { frame.open(); return; } // Create the media frame. frame = wp.media.frames.customBackground = wp.media({ // Set the title of the modal. title: $el.data('choose'), // Tell the modal to show only images. library: { type: 'image' }, // Customize the submit button. button: { // Set the text of the button. text: $el.data('update'), /* * Tell the button not to close the modal, since we're * going to refresh the page when the image is selected. */ close: false } }); /** * When an image is selected, run a callback. * * @since 3.5.0 * * @return {void} */ frame.on( 'select', function() { // Grab the selected attachment. var attachment = frame.state().get('selection').first(); var nonceValue = $( '#_wpnonce' ).val() || ''; // Run an Ajax request to set the background image. $.post( ajaxurl, { action: 'set-background-image', attachment_id: attachment.id, _ajax_nonce: nonceValue, size: 'full' }).done( function() { // When the request completes, reload the window. window.location.reload(); }); }); // Finally, open the modal. frame.open(); }); }); })(jQuery); PK1YZܖedit-comments.jsnu[/** * Handles updating and editing comments. * * @file This file contains functionality for the admin comments page. * @since 2.1.0 * @output wp-admin/js/edit-comments.js */ /* global adminCommentsSettings, thousandsSeparator, list_args, QTags, ajaxurl, wpAjax */ /* global commentReply, theExtraList, theList, setCommentsList */ (function($) { var getCount, updateCount, updateCountText, updatePending, updateApproved, updateHtmlTitle, updateDashboardText, updateInModerationText, adminTitle = document.title, isDashboard = $('#dashboard_right_now').length, titleDiv, titleRegEx, __ = wp.i18n.__; /** * Extracts a number from the content of a jQuery element. * * @since 2.9.0 * @access private * * @param {jQuery} el jQuery element. * * @return {number} The number found in the given element. */ getCount = function(el) { var n = parseInt( el.html().replace(/[^0-9]+/g, ''), 10 ); if ( isNaN(n) ) { return 0; } return n; }; /** * Updates an html element with a localized number string. * * @since 2.9.0 * @access private * * @param {jQuery} el The jQuery element to update. * @param {number} n Number to be put in the element. * * @return {void} */ updateCount = function(el, n) { var n1 = ''; if ( isNaN(n) ) { return; } n = n < 1 ? '0' : n.toString(); if ( n.length > 3 ) { while ( n.length > 3 ) { n1 = thousandsSeparator + n.substr(n.length - 3) + n1; n = n.substr(0, n.length - 3); } n = n + n1; } el.html(n); }; /** * Updates the number of approved comments on a specific post and the filter bar. * * @since 4.4.0 * @access private * * @param {number} diff The amount to lower or raise the approved count with. * @param {number} commentPostId The ID of the post to be updated. * * @return {void} */ updateApproved = function( diff, commentPostId ) { var postSelector = '.post-com-count-' + commentPostId, noClass = 'comment-count-no-comments', approvedClass = 'comment-count-approved', approved, noComments; updateCountText( 'span.approved-count', diff ); if ( ! commentPostId ) { return; } // Cache selectors to not get duplicates. approved = $( 'span.' + approvedClass, postSelector ); noComments = $( 'span.' + noClass, postSelector ); approved.each(function() { var a = $(this), n = getCount(a) + diff; if ( n < 1 ) n = 0; if ( 0 === n ) { a.removeClass( approvedClass ).addClass( noClass ); } else { a.addClass( approvedClass ).removeClass( noClass ); } updateCount( a, n ); }); noComments.each(function() { var a = $(this); if ( diff > 0 ) { a.removeClass( noClass ).addClass( approvedClass ); } else { a.addClass( noClass ).removeClass( approvedClass ); } updateCount( a, diff ); }); }; /** * Updates a number count in all matched HTML elements * * @since 4.4.0 * @access private * * @param {string} selector The jQuery selector for elements to update a count * for. * @param {number} diff The amount to lower or raise the count with. * * @return {void} */ updateCountText = function( selector, diff ) { $( selector ).each(function() { var a = $(this), n = getCount(a) + diff; if ( n < 1 ) { n = 0; } updateCount( a, n ); }); }; /** * Updates a text about comment count on the dashboard. * * @since 4.4.0 * @access private * * @param {Object} response Ajax response from the server that includes a * translated "comment count" message. * * @return {void} */ updateDashboardText = function( response ) { if ( ! isDashboard || ! response || ! response.i18n_comments_text ) { return; } $( '.comment-count a', '#dashboard_right_now' ).text( response.i18n_comments_text ); }; /** * Updates the "comments in moderation" text across the UI. * * @since 5.2.0 * * @param {Object} response Ajax response from the server that includes a * translated "comments in moderation" message. * * @return {void} */ updateInModerationText = function( response ) { if ( ! response || ! response.i18n_moderation_text ) { return; } // Update the "comment in moderation" text across the UI. $( '.comments-in-moderation-text' ).text( response.i18n_moderation_text ); // Hide the "comment in moderation" text in the Dashboard "At a Glance" widget. if ( isDashboard && response.in_moderation ) { $( '.comment-mod-count', '#dashboard_right_now' ) [ response.in_moderation > 0 ? 'removeClass' : 'addClass' ]( 'hidden' ); } }; /** * Updates the title of the document with the number comments to be approved. * * @since 4.4.0 * @access private * * @param {number} diff The amount to lower or raise the number of to be * approved comments with. * * @return {void} */ updateHtmlTitle = function( diff ) { var newTitle, regExMatch, titleCount, commentFrag; /* translators: %s: Comments count. */ titleRegEx = titleRegEx || new RegExp( __( 'Comments (%s)' ).replace( '%s', '\\([0-9' + thousandsSeparator + ']+\\)' ) + '?' ); // Count funcs operate on a $'d element. titleDiv = titleDiv || $( '
' ); newTitle = adminTitle; commentFrag = titleRegEx.exec( document.title ); if ( commentFrag ) { commentFrag = commentFrag[0]; titleDiv.html( commentFrag ); titleCount = getCount( titleDiv ) + diff; } else { titleDiv.html( 0 ); titleCount = diff; } if ( titleCount >= 1 ) { updateCount( titleDiv, titleCount ); regExMatch = titleRegEx.exec( document.title ); if ( regExMatch ) { /* translators: %s: Comments count. */ newTitle = document.title.replace( regExMatch[0], __( 'Comments (%s)' ).replace( '%s', titleDiv.text() ) + ' ' ); } } else { regExMatch = titleRegEx.exec( newTitle ); if ( regExMatch ) { newTitle = newTitle.replace( regExMatch[0], __( 'Comments' ) ); } } document.title = newTitle; }; /** * Updates the number of pending comments on a specific post and the filter bar. * * @since 3.2.0 * @access private * * @param {number} diff The amount to lower or raise the pending count with. * @param {number} commentPostId The ID of the post to be updated. * * @return {void} */ updatePending = function( diff, commentPostId ) { var postSelector = '.post-com-count-' + commentPostId, noClass = 'comment-count-no-pending', noParentClass = 'post-com-count-no-pending', pendingClass = 'comment-count-pending', pending, noPending; if ( ! isDashboard ) { updateHtmlTitle( diff ); } $( 'span.pending-count' ).each(function() { var a = $(this), n = getCount(a) + diff; if ( n < 1 ) n = 0; a.closest('.awaiting-mod')[ 0 === n ? 'addClass' : 'removeClass' ]('count-0'); updateCount( a, n ); }); if ( ! commentPostId ) { return; } // Cache selectors to not get dupes. pending = $( 'span.' + pendingClass, postSelector ); noPending = $( 'span.' + noClass, postSelector ); pending.each(function() { var a = $(this), n = getCount(a) + diff; if ( n < 1 ) n = 0; if ( 0 === n ) { a.parent().addClass( noParentClass ); a.removeClass( pendingClass ).addClass( noClass ); } else { a.parent().removeClass( noParentClass ); a.addClass( pendingClass ).removeClass( noClass ); } updateCount( a, n ); }); noPending.each(function() { var a = $(this); if ( diff > 0 ) { a.parent().removeClass( noParentClass ); a.removeClass( noClass ).addClass( pendingClass ); } else { a.parent().addClass( noParentClass ); a.addClass( noClass ).removeClass( pendingClass ); } updateCount( a, diff ); }); }; /** * Initializes the comments list. * * @since 4.4.0 * * @global * * @return {void} */ window.setCommentsList = function() { var totalInput, perPageInput, pageInput, dimAfter, delBefore, updateTotalCount, delAfter, refillTheExtraList, diff, lastConfidentTime = 0; totalInput = $('input[name="_total"]', '#comments-form'); perPageInput = $('input[name="_per_page"]', '#comments-form'); pageInput = $('input[name="_page"]', '#comments-form'); /** * Updates the total with the latest count. * * The time parameter makes sure that we only update the total if this value is * a newer value than we previously received. * * The time and setConfidentTime parameters make sure that we only update the * total when necessary. So a value that has been generated earlier will not * update the total. * * @since 2.8.0 * @access private * * @param {number} total Total number of comments. * @param {number} time Unix timestamp of response. * @param {boolean} setConfidentTime Whether to update the last confident time * with the given time. * * @return {void} */ updateTotalCount = function( total, time, setConfidentTime ) { if ( time < lastConfidentTime ) return; if ( setConfidentTime ) lastConfidentTime = time; totalInput.val( total.toString() ); }; /** * Changes DOM that need to be changed after a list item has been dimmed. * * @since 2.5.0 * @access private * * @param {Object} r Ajax response object. * @param {Object} settings Settings for the wpList object. * * @return {void} */ dimAfter = function( r, settings ) { var editRow, replyID, replyButton, response, c = $( '#' + settings.element ); if ( true !== settings.parsed ) { response = settings.parsed.responses[0]; } editRow = $('#replyrow'); replyID = $('#comment_ID', editRow).val(); replyButton = $('#replybtn', editRow); if ( c.is('.unapproved') ) { if ( settings.data.id == replyID ) replyButton.text( __( 'Approve and Reply' ) ); c.find( '.row-actions span.view' ).addClass( 'hidden' ).end() .find( 'div.comment_status' ).html( '0' ); } else { if ( settings.data.id == replyID ) replyButton.text( __( 'Reply' ) ); c.find( '.row-actions span.view' ).removeClass( 'hidden' ).end() .find( 'div.comment_status' ).html( '1' ); } diff = $('#' + settings.element).is('.' + settings.dimClass) ? 1 : -1; if ( response ) { updateDashboardText( response.supplemental ); updateInModerationText( response.supplemental ); updatePending( diff, response.supplemental.postId ); updateApproved( -1 * diff, response.supplemental.postId ); } else { updatePending( diff ); updateApproved( -1 * diff ); } }; /** * Handles marking a comment as spam or trashing the comment. * * Is executed in the list delBefore hook. * * @since 2.8.0 * @access private * * @param {Object} settings Settings for the wpList object. * @param {HTMLElement} list Comments table element. * * @return {Object} The settings object. */ delBefore = function( settings, list ) { var note, id, el, n, h, a, author, action = false, wpListsData = $( settings.target ).attr( 'data-wp-lists' ); settings.data._total = totalInput.val() || 0; settings.data._per_page = perPageInput.val() || 0; settings.data._page = pageInput.val() || 0; settings.data._url = document.location.href; settings.data.comment_status = $('input[name="comment_status"]', '#comments-form').val(); if ( wpListsData.indexOf(':trash=1') != -1 ) action = 'trash'; else if ( wpListsData.indexOf(':spam=1') != -1 ) action = 'spam'; if ( action ) { id = wpListsData.replace(/.*?comment-([0-9]+).*/, '$1'); el = $('#comment-' + id); note = $('#' + action + '-undo-holder').html(); el.find('.check-column :checkbox').prop('checked', false); // Uncheck the row so as not to be affected by Bulk Edits. if ( el.siblings('#replyrow').length && commentReply.cid == id ) commentReply.close(); if ( el.is('tr') ) { n = el.children(':visible').length; author = $('.author strong', el).text(); h = $('' + note + ''); } else { author = $('.comment-author', el).text(); h = $(''); } el.before(h); $('strong', '#undo-' + id).text(author); a = $('.undo a', '#undo-' + id); a.attr('href', 'comment.php?action=un' + action + 'comment&c=' + id + '&_wpnonce=' + settings.data._ajax_nonce); a.attr('data-wp-lists', 'delete:the-comment-list:comment-' + id + '::un' + action + '=1'); a.attr('class', 'vim-z vim-destructive aria-button-if-js'); $('.avatar', el).first().clone().prependTo('#undo-' + id + ' .' + action + '-undo-inside'); a.on( 'click', function( e ){ e.preventDefault(); e.stopPropagation(); // Ticket #35904. list.wpList.del(this); $('#undo-' + id).css( {backgroundColor:'#ceb'} ).fadeOut(350, function(){ $(this).remove(); $('#comment-' + id).css('backgroundColor', '').fadeIn(300, function(){ $(this).show(); }); }); }); } return settings; }; /** * Handles actions that need to be done after marking as spam or thrashing a * comment. * * The ajax requests return the unix time stamp a comment was marked as spam or * trashed. We use this to have a correct total amount of comments. * * @since 2.5.0 * @access private * * @param {Object} r Ajax response object. * @param {Object} settings Settings for the wpList object. * * @return {void} */ delAfter = function( r, settings ) { var total_items_i18n, total, animated, animatedCallback, response = true === settings.parsed ? {} : settings.parsed.responses[0], commentStatus = true === settings.parsed ? '' : response.supplemental.status, commentPostId = true === settings.parsed ? '' : response.supplemental.postId, newTotal = true === settings.parsed ? '' : response.supplemental, targetParent = $( settings.target ).parent(), commentRow = $('#' + settings.element), spamDiff, trashDiff, pendingDiff, approvedDiff, /* * As `wpList` toggles only the `unapproved` class, the approved comment * rows can have both the `approved` and `unapproved` classes. */ approved = commentRow.hasClass( 'approved' ) && ! commentRow.hasClass( 'unapproved' ), unapproved = commentRow.hasClass( 'unapproved' ), spammed = commentRow.hasClass( 'spam' ), trashed = commentRow.hasClass( 'trash' ), undoing = false; // Ticket #35904. updateDashboardText( newTotal ); updateInModerationText( newTotal ); /* * The order of these checks is important. * .unspam can also have .approve or .unapprove. * .untrash can also have .approve or .unapprove. */ if ( targetParent.is( 'span.undo' ) ) { // The comment was spammed. if ( targetParent.hasClass( 'unspam' ) ) { spamDiff = -1; if ( 'trash' === commentStatus ) { trashDiff = 1; } else if ( '1' === commentStatus ) { approvedDiff = 1; } else if ( '0' === commentStatus ) { pendingDiff = 1; } // The comment was trashed. } else if ( targetParent.hasClass( 'untrash' ) ) { trashDiff = -1; if ( 'spam' === commentStatus ) { spamDiff = 1; } else if ( '1' === commentStatus ) { approvedDiff = 1; } else if ( '0' === commentStatus ) { pendingDiff = 1; } } undoing = true; // User clicked "Spam". } else if ( targetParent.is( 'span.spam' ) ) { // The comment is currently approved. if ( approved ) { approvedDiff = -1; // The comment is currently pending. } else if ( unapproved ) { pendingDiff = -1; // The comment was in the Trash. } else if ( trashed ) { trashDiff = -1; } // You can't spam an item on the Spam screen. spamDiff = 1; // User clicked "Unspam". } else if ( targetParent.is( 'span.unspam' ) ) { if ( approved ) { pendingDiff = 1; } else if ( unapproved ) { approvedDiff = 1; } else if ( trashed ) { // The comment was previously approved. if ( targetParent.hasClass( 'approve' ) ) { approvedDiff = 1; // The comment was previously pending. } else if ( targetParent.hasClass( 'unapprove' ) ) { pendingDiff = 1; } } else if ( spammed ) { if ( targetParent.hasClass( 'approve' ) ) { approvedDiff = 1; } else if ( targetParent.hasClass( 'unapprove' ) ) { pendingDiff = 1; } } // You can unspam an item on the Spam screen. spamDiff = -1; // User clicked "Trash". } else if ( targetParent.is( 'span.trash' ) ) { if ( approved ) { approvedDiff = -1; } else if ( unapproved ) { pendingDiff = -1; // The comment was in the spam queue. } else if ( spammed ) { spamDiff = -1; } // You can't trash an item on the Trash screen. trashDiff = 1; // User clicked "Restore". } else if ( targetParent.is( 'span.untrash' ) ) { if ( approved ) { pendingDiff = 1; } else if ( unapproved ) { approvedDiff = 1; } else if ( trashed ) { if ( targetParent.hasClass( 'approve' ) ) { approvedDiff = 1; } else if ( targetParent.hasClass( 'unapprove' ) ) { pendingDiff = 1; } } // You can't go from Trash to Spam. // You can untrash on the Trash screen. trashDiff = -1; // User clicked "Approve". } else if ( targetParent.is( 'span.approve:not(.unspam):not(.untrash)' ) ) { approvedDiff = 1; pendingDiff = -1; // User clicked "Unapprove". } else if ( targetParent.is( 'span.unapprove:not(.unspam):not(.untrash)' ) ) { approvedDiff = -1; pendingDiff = 1; // User clicked "Delete Permanently". } else if ( targetParent.is( 'span.delete' ) ) { if ( spammed ) { spamDiff = -1; } else if ( trashed ) { trashDiff = -1; } } if ( pendingDiff ) { updatePending( pendingDiff, commentPostId ); updateCountText( 'span.all-count', pendingDiff ); } if ( approvedDiff ) { updateApproved( approvedDiff, commentPostId ); updateCountText( 'span.all-count', approvedDiff ); } if ( spamDiff ) { updateCountText( 'span.spam-count', spamDiff ); } if ( trashDiff ) { updateCountText( 'span.trash-count', trashDiff ); } if ( ( ( 'trash' === settings.data.comment_status ) && !getCount( $( 'span.trash-count' ) ) ) || ( ( 'spam' === settings.data.comment_status ) && !getCount( $( 'span.spam-count' ) ) ) ) { $( '#delete_all' ).hide(); } if ( ! isDashboard ) { total = totalInput.val() ? parseInt( totalInput.val(), 10 ) : 0; if ( $(settings.target).parent().is('span.undo') ) total++; else total--; if ( total < 0 ) total = 0; if ( 'object' === typeof r ) { if ( response.supplemental.total_items_i18n && lastConfidentTime < response.supplemental.time ) { total_items_i18n = response.supplemental.total_items_i18n || ''; if ( total_items_i18n ) { $('.displaying-num').text( total_items_i18n.replace( ' ', String.fromCharCode( 160 ) ) ); $('.total-pages').text( response.supplemental.total_pages_i18n.replace( ' ', String.fromCharCode( 160 ) ) ); $('.tablenav-pages').find('.next-page, .last-page').toggleClass('disabled', response.supplemental.total_pages == $('.current-page').val()); } updateTotalCount( total, response.supplemental.time, true ); } else if ( response.supplemental.time ) { updateTotalCount( total, response.supplemental.time, false ); } } else { updateTotalCount( total, r, false ); } } if ( ! theExtraList || theExtraList.length === 0 || theExtraList.children().length === 0 || undoing ) { return; } theList.get(0).wpList.add( theExtraList.children( ':eq(0):not(.no-items)' ).remove().clone() ); refillTheExtraList(); animated = $( ':animated', '#the-comment-list' ); animatedCallback = function() { if ( ! $( '#the-comment-list tr:visible' ).length ) { theList.get(0).wpList.add( theExtraList.find( '.no-items' ).clone() ); } }; if ( animated.length ) { animated.promise().done( animatedCallback ); } else { animatedCallback(); } }; /** * Retrieves additional comments to populate the extra list. * * @since 3.1.0 * @access private * * @param {boolean} [ev] Repopulate the extra comments list if true. * * @return {void} */ refillTheExtraList = function(ev) { var args = $.query.get(), total_pages = $('.total-pages').text(), per_page = $('input[name="_per_page"]', '#comments-form').val(); if (! args.paged) args.paged = 1; if (args.paged > total_pages) { return; } if (ev) { theExtraList.empty(); args.number = Math.min(8, per_page); // See WP_Comments_List_Table::prepare_items() in class-wp-comments-list-table.php. } else { args.number = 1; args.offset = Math.min(8, per_page) - 1; // Fetch only the next item on the extra list. } args.no_placeholder = true; args.paged ++; // $.query.get() needs some correction to be sent into an Ajax request. if ( true === args.comment_type ) args.comment_type = ''; args = $.extend(args, { 'action': 'fetch-list', 'list_args': list_args, '_ajax_fetch_list_nonce': $('#_ajax_fetch_list_nonce').val() }); $.ajax({ url: ajaxurl, global: false, dataType: 'json', data: args, success: function(response) { theExtraList.get(0).wpList.add( response.rows ); } }); }; /** * Globally available jQuery object referring to the extra comments list. * * @global */ window.theExtraList = $('#the-extra-comment-list').wpList( { alt: '', delColor: 'none', addColor: 'none' } ); /** * Globally available jQuery object referring to the comments list. * * @global */ window.theList = $('#the-comment-list').wpList( { alt: '', delBefore: delBefore, dimAfter: dimAfter, delAfter: delAfter, addColor: 'none' } ) .on('wpListDelEnd', function(e, s){ var wpListsData = $(s.target).attr('data-wp-lists'), id = s.element.replace(/[^0-9]+/g, ''); if ( wpListsData.indexOf(':trash=1') != -1 || wpListsData.indexOf(':spam=1') != -1 ) $('#undo-' + id).fadeIn(300, function(){ $(this).show(); }); }); }; /** * Object containing functionality regarding the comment quick editor and reply * editor. * * @since 2.7.0 * * @global */ window.commentReply = { cid : '', act : '', originalContent : '', /** * Initializes the comment reply functionality. * * @since 2.7.0 * * @memberof commentReply */ init : function() { var row = $('#replyrow'); $( '.cancel', row ).on( 'click', function() { return commentReply.revert(); } ); $( '.save', row ).on( 'click', function() { return commentReply.send(); } ); $( 'input#author-name, input#author-email, input#author-url', row ).on( 'keypress', function( e ) { if ( e.which == 13 ) { commentReply.send(); e.preventDefault(); return false; } }); // Add events. $('#the-comment-list .column-comment > p').on( 'dblclick', function(){ commentReply.toggle($(this).parent()); }); $('#doaction, #post-query-submit').on( 'click', function(){ if ( $('#the-comment-list #replyrow').length > 0 ) commentReply.close(); }); this.comments_listing = $('#comments-form > input[name="comment_status"]').val() || ''; }, /** * Adds doubleclick event handler to the given comment list row. * * The double-click event will toggle the comment edit or reply form. * * @since 2.7.0 * * @memberof commentReply * * @param {Object} r The row to add double click handlers to. * * @return {void} */ addEvents : function(r) { r.each(function() { $(this).find('.column-comment > p').on( 'dblclick', function(){ commentReply.toggle($(this).parent()); }); }); }, /** * Opens the quick edit for the given element. * * @since 2.7.0 * * @memberof commentReply * * @param {HTMLElement} el The element you want to open the quick editor for. * * @return {void} */ toggle : function(el) { if ( 'none' !== $( el ).css( 'display' ) && ( $( '#replyrow' ).parent().is('#com-reply') || window.confirm( __( 'Are you sure you want to edit this comment?\nThe changes you made will be lost.' ) ) ) ) { $( el ).find( 'button.vim-q' ).trigger( 'click' ); } }, /** * Closes the comment quick edit or reply form and undoes any changes. * * @since 2.7.0 * * @memberof commentReply * * @return {void} */ revert : function() { if ( $('#the-comment-list #replyrow').length < 1 ) return false; $('#replyrow').fadeOut('fast', function(){ commentReply.close(); }); }, /** * Closes the comment quick edit or reply form and undoes any changes. * * @since 2.7.0 * * @memberof commentReply * * @return {void} */ close : function() { var commentRow = $(), replyRow = $( '#replyrow' ); // Return if the replyrow is not showing. if ( replyRow.parent().is( '#com-reply' ) ) { return; } if ( this.cid ) { commentRow = $( '#comment-' + this.cid ); } /* * When closing the Quick Edit form, show the comment row and move focus * back to the Quick Edit button. */ if ( 'edit-comment' === this.act ) { commentRow.fadeIn( 300, function() { commentRow .show() .find( '.vim-q' ) .attr( 'aria-expanded', 'false' ) .trigger( 'focus' ); } ).css( 'backgroundColor', '' ); } // When closing the Reply form, move focus back to the Reply button. if ( 'replyto-comment' === this.act ) { commentRow.find( '.vim-r' ) .attr( 'aria-expanded', 'false' ) .trigger( 'focus' ); } // Reset the Quicktags buttons. if ( typeof QTags != 'undefined' ) QTags.closeAllTags('replycontent'); $('#add-new-comment').css('display', ''); replyRow.hide(); $( '#com-reply' ).append( replyRow ); $('#replycontent').css('height', '').val(''); $('#edithead input').val(''); $( '.notice-error', replyRow ) .addClass( 'hidden' ) .find( '.error' ).empty(); $( '.spinner', replyRow ).removeClass( 'is-active' ); this.cid = ''; this.originalContent = ''; }, /** * Opens the comment quick edit or reply form. * * @since 2.7.0 * * @memberof commentReply * * @param {number} comment_id The comment ID to open an editor for. * @param {number} post_id The post ID to open an editor for. * @param {string} action The action to perform. Either 'edit' or 'replyto'. * * @return {boolean} Always false. */ open : function(comment_id, post_id, action) { var editRow, rowData, act, replyButton, editHeight, t = this, c = $('#comment-' + comment_id), h = c.height(), colspanVal = 0; if ( ! this.discardCommentChanges() ) { return false; } t.close(); t.cid = comment_id; editRow = $('#replyrow'); rowData = $('#inline-'+comment_id); action = action || 'replyto'; act = 'edit' == action ? 'edit' : 'replyto'; act = t.act = act + '-comment'; t.originalContent = $('textarea.comment', rowData).val(); colspanVal = $( '> th:visible, > td:visible', c ).length; // Make sure it's actually a table and there's a `colspan` value to apply. if ( editRow.hasClass( 'inline-edit-row' ) && 0 !== colspanVal ) { $( 'td', editRow ).attr( 'colspan', colspanVal ); } $('#action', editRow).val(act); $('#comment_post_ID', editRow).val(post_id); $('#comment_ID', editRow).val(comment_id); if ( action == 'edit' ) { $( '#author-name', editRow ).val( $( 'div.author', rowData ).text() ); $('#author-email', editRow).val( $('div.author-email', rowData).text() ); $('#author-url', editRow).val( $('div.author-url', rowData).text() ); $('#status', editRow).val( $('div.comment_status', rowData).text() ); $('#replycontent', editRow).val( $('textarea.comment', rowData).val() ); $( '#edithead, #editlegend, #savebtn', editRow ).show(); $('#replyhead, #replybtn, #addhead, #addbtn', editRow).hide(); if ( h > 120 ) { // Limit the maximum height when editing very long comments to make it more manageable. // The textarea is resizable in most browsers, so the user can adjust it if needed. editHeight = h > 500 ? 500 : h; $('#replycontent', editRow).css('height', editHeight + 'px'); } c.after( editRow ).fadeOut('fast', function(){ $('#replyrow').fadeIn(300, function(){ $(this).show(); }); }); } else if ( action == 'add' ) { $('#addhead, #addbtn', editRow).show(); $( '#replyhead, #replybtn, #edithead, #editlegend, #savebtn', editRow ) .hide(); $('#the-comment-list').prepend(editRow); $('#replyrow').fadeIn(300); } else { replyButton = $('#replybtn', editRow); $( '#edithead, #editlegend, #savebtn, #addhead, #addbtn', editRow ).hide(); $('#replyhead, #replybtn', editRow).show(); c.after(editRow); if ( c.hasClass('unapproved') ) { replyButton.text( __( 'Approve and Reply' ) ); } else { replyButton.text( __( 'Reply' ) ); } $('#replyrow').fadeIn(300, function(){ $(this).show(); }); } setTimeout(function() { var rtop, rbottom, scrollTop, vp, scrollBottom, isComposing = false; rtop = $('#replyrow').offset().top; rbottom = rtop + $('#replyrow').height(); scrollTop = window.pageYOffset || document.documentElement.scrollTop; vp = document.documentElement.clientHeight || window.innerHeight || 0; scrollBottom = scrollTop + vp; if ( scrollBottom - 20 < rbottom ) window.scroll(0, rbottom - vp + 35); else if ( rtop - 20 < scrollTop ) window.scroll(0, rtop - 35); $( '#replycontent' ) .trigger( 'focus' ) .on( 'keyup', function( e ) { // Close on Escape except when Input Method Editors (IMEs) are in use. if ( e.which === 27 && ! isComposing ) { commentReply.revert(); } } ) .on( 'compositionstart', function() { isComposing = true; } ); }, 600); return false; }, /** * Submits the comment quick edit or reply form. * * @since 2.7.0 * * @memberof commentReply * * @return {void} */ send : function() { var post = {}, $errorNotice = $( '#replysubmit .error-notice' ); $errorNotice.addClass( 'hidden' ); $( '#replysubmit .spinner' ).addClass( 'is-active' ); $('#replyrow input').not(':button').each(function() { var t = $(this); post[ t.attr('name') ] = t.val(); }); post.content = $('#replycontent').val(); post.id = post.comment_post_ID; post.comments_listing = this.comments_listing; post.p = $('[name="p"]').val(); if ( $('#comment-' + $('#comment_ID').val()).hasClass('unapproved') ) post.approve_parent = 1; $.ajax({ type : 'POST', url : ajaxurl, data : post, success : function(x) { commentReply.show(x); }, error : function(r) { commentReply.error(r); } }); }, /** * Shows the new or updated comment or reply. * * This function needs to be passed the ajax result as received from the server. * It will handle the response and show the comment that has just been saved to * the server. * * @since 2.7.0 * * @memberof commentReply * * @param {Object} xml Ajax response object. * * @return {void} */ show : function(xml) { var t = this, r, c, id, bg, pid; if ( typeof(xml) == 'string' ) { t.error({'responseText': xml}); return false; } r = wpAjax.parseAjaxResponse(xml); if ( r.errors ) { t.error({'responseText': wpAjax.broken}); return false; } t.revert(); r = r.responses[0]; id = '#comment-' + r.id; if ( 'edit-comment' == t.act ) $(id).remove(); if ( r.supplemental.parent_approved ) { pid = $('#comment-' + r.supplemental.parent_approved); updatePending( -1, r.supplemental.parent_post_id ); if ( this.comments_listing == 'moderated' ) { pid.animate( { 'backgroundColor':'#CCEEBB' }, 400, function(){ pid.fadeOut(); }); return; } } if ( r.supplemental.i18n_comments_text ) { updateDashboardText( r.supplemental ); updateInModerationText( r.supplemental ); updateApproved( 1, r.supplemental.parent_post_id ); updateCountText( 'span.all-count', 1 ); } r.data = r.data || ''; c = r.data.toString().trim(); // Trim leading whitespaces. $(c).hide(); $('#replyrow').after(c); id = $(id); t.addEvents(id); bg = id.hasClass('unapproved') ? '#FFFFE0' : id.closest('.widefat, .postbox').css('backgroundColor'); id.animate( { 'backgroundColor':'#CCEEBB' }, 300 ) .animate( { 'backgroundColor': bg }, 300, function() { if ( pid && pid.length ) { pid.animate( { 'backgroundColor':'#CCEEBB' }, 300 ) .animate( { 'backgroundColor': bg }, 300 ) .removeClass('unapproved').addClass('approved') .find('div.comment_status').html('1'); } }); }, /** * Shows an error for the failed comment update or reply. * * @since 2.7.0 * * @memberof commentReply * * @param {string} r The Ajax response. * * @return {void} */ error : function(r) { var er = r.statusText, $errorNotice = $( '#replysubmit .notice-error' ), $error = $errorNotice.find( '.error' ); $( '#replysubmit .spinner' ).removeClass( 'is-active' ); if ( r.responseText ) er = r.responseText.replace( /<.[^<>]*?>/g, '' ); if ( er ) { $errorNotice.removeClass( 'hidden' ); $error.html( er ); wp.a11y.speak( er ); } }, /** * Opens the add comments form in the comments metabox on the post edit page. * * @since 3.4.0 * * @memberof commentReply * * @param {number} post_id The post ID. * * @return {void} */ addcomment: function(post_id) { var t = this; $('#add-new-comment').fadeOut(200, function(){ t.open(0, post_id, 'add'); $('table.comments-box').css('display', ''); $('#no-comments').remove(); }); }, /** * Alert the user if they have unsaved changes on a comment that will be lost if * they proceed with the intended action. * * @since 4.6.0 * * @memberof commentReply * * @return {boolean} Whether it is safe the continue with the intended action. */ discardCommentChanges: function() { var editRow = $( '#replyrow' ); if ( '' === $( '#replycontent', editRow ).val() || this.originalContent === $( '#replycontent', editRow ).val() ) { return true; } return window.confirm( __( 'Are you sure you want to do this?\nThe comment changes you made will be lost.' ) ); } }; $( function(){ var make_hotkeys_redirect, edit_comment, toggle_all, make_bulk; setCommentsList(); commentReply.init(); $(document).on( 'click', 'span.delete a.delete', function( e ) { e.preventDefault(); }); if ( typeof $.table_hotkeys != 'undefined' ) { /** * Creates a function that navigates to a previous or next page. * * @since 2.7.0 * @access private * * @param {string} which What page to navigate to: either next or prev. * * @return {Function} The function that executes the navigation. */ make_hotkeys_redirect = function(which) { return function() { var first_last, l; first_last = 'next' == which? 'first' : 'last'; l = $('.tablenav-pages .'+which+'-page:not(.disabled)'); if (l.length) window.location = l[0].href.replace(/\&hotkeys_highlight_(first|last)=1/g, '')+'&hotkeys_highlight_'+first_last+'=1'; }; }; /** * Navigates to the edit page for the selected comment. * * @since 2.7.0 * @access private * * @param {Object} event The event that triggered this action. * @param {Object} current_row A jQuery object of the selected row. * * @return {void} */ edit_comment = function(event, current_row) { window.location = $('span.edit a', current_row).attr('href'); }; /** * Toggles all comments on the screen, for bulk actions. * * @since 2.7.0 * @access private * * @return {void} */ toggle_all = function() { $('#cb-select-all-1').data( 'wp-toggle', 1 ).trigger( 'click' ).removeData( 'wp-toggle' ); }; /** * Creates a bulk action function that is executed on all selected comments. * * @since 2.7.0 * @access private * * @param {string} value The name of the action to execute. * * @return {Function} The function that executes the bulk action. */ make_bulk = function(value) { return function() { var scope = $('select[name="action"]'); $('option[value="' + value + '"]', scope).prop('selected', true); $('#doaction').trigger( 'click' ); }; }; $.table_hotkeys( $('table.widefat'), [ 'a', 'u', 's', 'd', 'r', 'q', 'z', ['e', edit_comment], ['shift+x', toggle_all], ['shift+a', make_bulk('approve')], ['shift+s', make_bulk('spam')], ['shift+d', make_bulk('delete')], ['shift+t', make_bulk('trash')], ['shift+z', make_bulk('untrash')], ['shift+u', make_bulk('unapprove')] ], { highlight_first: adminCommentsSettings.hotkeys_highlight_first, highlight_last: adminCommentsSettings.hotkeys_highlight_last, prev_page_link_cb: make_hotkeys_redirect('prev'), next_page_link_cb: make_hotkeys_redirect('next'), hotkeys_opts: { disableInInput: true, type: 'keypress', noDisable: '.check-column input[type="checkbox"]' }, cycle_expr: '#the-comment-list tr', start_row_index: 0 } ); } // Quick Edit and Reply have an inline comment editor. $( '#the-comment-list' ).on( 'click', '.comment-inline', function() { var $el = $( this ), action = 'replyto'; if ( 'undefined' !== typeof $el.data( 'action' ) ) { action = $el.data( 'action' ); } $( this ).attr( 'aria-expanded', 'true' ); commentReply.open( $el.data( 'commentId' ), $el.data( 'postId' ), action ); } ); }); })(jQuery); PK1YZku u accordion.jsnu[/** * Accordion-folding functionality. * * Markup with the appropriate classes will be automatically hidden, * with one section opening at a time when its title is clicked. * Use the following markup structure for accordion behavior: * *
*
*

*
*
*
*
*

*
*
*
*
*

*
*
*
*
* * Note that any appropriate tags may be used, as long as the above classes are present. * * @since 3.6.0 * @output wp-admin/js/accordion.js */ ( function( $ ){ $( function () { // Expand/Collapse accordion sections on click. $( '.accordion-container' ).on( 'click', '.accordion-section-title button', function() { accordionSwitch( $( this ) ); }); }); /** * Close the current accordion section and open a new one. * * @param {Object} el Title element of the accordion section to toggle. * @since 3.6.0 */ function accordionSwitch ( el ) { var section = el.closest( '.accordion-section' ), container = section.closest( '.accordion-container' ), siblings = container.find( '.open' ), siblingsToggleControl = siblings.find( '[aria-expanded]' ).first(), content = section.find( '.accordion-section-content' ); // This section has no content and cannot be expanded. if ( section.hasClass( 'cannot-expand' ) ) { return; } // Add a class to the container to let us know something is happening inside. // This helps in cases such as hiding a scrollbar while animations are executing. container.addClass( 'opening' ); if ( section.hasClass( 'open' ) ) { section.toggleClass( 'open' ); content.toggle( true ).slideToggle( 150 ); } else { siblingsToggleControl.attr( 'aria-expanded', 'false' ); siblings.removeClass( 'open' ); siblings.find( '.accordion-section-content' ).show().slideUp( 150 ); content.toggle( false ).slideToggle( 150 ); section.toggleClass( 'open' ); } // We have to wait for the animations to finish. setTimeout(function(){ container.removeClass( 'opening' ); }, 150); // If there's an element with an aria-expanded attribute, assume it's a toggle control and toggle the aria-expanded value. if ( el ) { el.attr( 'aria-expanded', String( el.attr( 'aria-expanded' ) === 'false' ) ); } } })(jQuery); PK1YZ-uuinline-edit-tax.jsnu[/** * This file is used on the term overview page to power quick-editing terms. * * @output wp-admin/js/inline-edit-tax.js */ /* global ajaxurl, inlineEditTax */ window.wp = window.wp || {}; /** * Consists of functions relevant to the inline taxonomy editor. * * @namespace inlineEditTax * * @property {string} type The type of inline edit we are currently on. * @property {string} what The type property with a hash prefixed and a dash * suffixed. */ ( function( $, wp ) { window.inlineEditTax = { /** * Initializes the inline taxonomy editor by adding event handlers to be able to * quick edit. * * @since 2.7.0 * * @this inlineEditTax * @memberof inlineEditTax * @return {void} */ init : function() { var t = this, row = $('#inline-edit'); t.type = $('#the-list').attr('data-wp-lists').substr(5); t.what = '#'+t.type+'-'; $( '#the-list' ).on( 'click', '.editinline', function() { $( this ).attr( 'aria-expanded', 'true' ); inlineEditTax.edit( this ); }); /** * Cancels inline editing when pressing Escape inside the inline editor. * * @param {Object} e The keyup event that has been triggered. */ row.on( 'keyup', function( e ) { // 27 = [Escape]. if ( e.which === 27 ) { return inlineEditTax.revert(); } }); /** * Cancels inline editing when clicking the cancel button. */ $( '.cancel', row ).on( 'click', function() { return inlineEditTax.revert(); }); /** * Saves the inline edits when clicking the save button. */ $( '.save', row ).on( 'click', function() { return inlineEditTax.save(this); }); /** * Saves the inline edits when pressing Enter inside the inline editor. */ $( 'input, select', row ).on( 'keydown', function( e ) { // 13 = [Enter]. if ( e.which === 13 ) { return inlineEditTax.save( this ); } }); /** * Saves the inline edits on submitting the inline edit form. */ $( '#posts-filter input[type="submit"]' ).on( 'mousedown', function() { t.revert(); }); }, /** * Toggles the quick edit based on if it is currently shown or hidden. * * @since 2.7.0 * * @this inlineEditTax * @memberof inlineEditTax * * @param {HTMLElement} el An element within the table row or the table row * itself that we want to quick edit. * @return {void} */ toggle : function(el) { var t = this; $(t.what+t.getId(el)).css('display') === 'none' ? t.revert() : t.edit(el); }, /** * Shows the quick editor * * @since 2.7.0 * * @this inlineEditTax * @memberof inlineEditTax * * @param {string|HTMLElement} id The ID of the term we want to quick edit or an * element within the table row or the * table row itself. * @return {boolean} Always returns false. */ edit : function(id) { var editRow, rowData, val, t = this; t.revert(); // Makes sure we can pass an HTMLElement as the ID. if ( typeof(id) === 'object' ) { id = t.getId(id); } editRow = $('#inline-edit').clone(true), rowData = $('#inline_'+id); $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '.wp-list-table.widefat:first thead' ).length ); $(t.what+id).hide().after(editRow).after(''); val = $('.name', rowData); val.find( 'img' ).replaceWith( function() { return this.alt; } ); val = val.text(); $(':input[name="name"]', editRow).val( val ); val = $('.slug', rowData); val.find( 'img' ).replaceWith( function() { return this.alt; } ); val = val.text(); $(':input[name="slug"]', editRow).val( val ); $(editRow).attr('id', 'edit-'+id).addClass('inline-editor').show(); $('.ptitle', editRow).eq(0).trigger( 'focus' ); return false; }, /** * Saves the quick edit data. * * Saves the quick edit data to the server and replaces the table row with the * HTML retrieved from the server. * * @since 2.7.0 * * @this inlineEditTax * @memberof inlineEditTax * * @param {string|HTMLElement} id The ID of the term we want to quick edit or an * element within the table row or the * table row itself. * @return {boolean} Always returns false. */ save : function(id) { var params, fields, tax = $('input[name="taxonomy"]').val() || ''; // Makes sure we can pass an HTMLElement as the ID. if( typeof(id) === 'object' ) { id = this.getId(id); } $( 'table.widefat .spinner' ).addClass( 'is-active' ); params = { action: 'inline-save-tax', tax_type: this.type, tax_ID: id, taxonomy: tax }; fields = $('#edit-'+id).find(':input').serialize(); params = fields + '&' + $.param(params); // Do the Ajax request to save the data to the server. $.post( ajaxurl, params, /** * Handles the response from the server * * Handles the response from the server, replaces the table row with the response * from the server. * * @param {string} r The string with which to replace the table row. */ function(r) { var row, new_id, option_value, $errorNotice = $( '#edit-' + id + ' .inline-edit-save .notice-error' ), $error = $errorNotice.find( '.error' ); $( 'table.widefat .spinner' ).removeClass( 'is-active' ); if (r) { if ( -1 !== r.indexOf( '', { role: 'alert' } ); // Set up the notice div. resultDiv.addClass( 'notice inline' ); // Add a class indicating success or failure. resultDiv.addClass( 'notice-' + ( success ? 'success' : 'error' ) ); // Add the message, wrapping in a p tag, with a fadein to highlight each message. resultDiv.text( $( $.parseHTML( message ) ).text() ).wrapInner( '

'); // Disable the button when the callback has succeeded. $this.prop( 'disabled', success ); // Remove any previous notices. $this.siblings( '.notice' ).remove(); // Insert the notice. $this.before( resultDiv ); } function bindPasswordForm() { var $generateButton, $cancelButton; $pass1Row = $( '.user-pass1-wrap, .user-pass-wrap, .mailserver-pass-wrap, .reset-pass-submit' ); // Hide the confirm password field when JavaScript support is enabled. $('.user-pass2-wrap').hide(); $submitButton = $( '#submit, #wp-submit' ).on( 'click', function () { updateLock = false; }); $submitButtons = $submitButton.add( ' #createusersub' ); $weakRow = $( '.pw-weak' ); $weakCheckbox = $weakRow.find( '.pw-checkbox' ); $weakCheckbox.on( 'change', function() { $submitButtons.prop( 'disabled', ! $weakCheckbox.prop( 'checked' ) ); } ); $pass1 = $('#pass1, #mailserver_pass'); if ( $pass1.length ) { bindPass1(); } else { // Password field for the login form. $pass1 = $( '#user_pass' ); } /* * Fix a LastPass mismatch issue, LastPass only changes pass2. * * This fixes the issue by copying any changes from the hidden * pass2 field to the pass1 field, then running check_pass_strength. */ $pass2 = $( '#pass2' ).on( 'input', function () { if ( $pass2.val().length > 0 ) { $pass1.val( $pass2.val() ); $pass2.val(''); currentPass = ''; $pass1.trigger( 'pwupdate' ); } } ); // Disable hidden inputs to prevent autofill and submission. if ( $pass1.is( ':hidden' ) ) { $pass1.prop( 'disabled', true ); $pass2.prop( 'disabled', true ); } $passwordWrapper = $pass1Row.find( '.wp-pwd' ); $generateButton = $pass1Row.find( 'button.wp-generate-pw' ); bindToggleButton(); $generateButton.show(); $generateButton.on( 'click', function () { updateLock = true; // Make sure the password fields are shown. $generateButton.not( '.skip-aria-expanded' ).attr( 'aria-expanded', 'true' ); $passwordWrapper .show() .addClass( 'is-open' ); // Enable the inputs when showing. $pass1.attr( 'disabled', false ); $pass2.attr( 'disabled', false ); // Set the password to the generated value. generatePassword(); // Show generated password in plaintext by default. resetToggle ( false ); // Generate the next password and cache. wp.ajax.post( 'generate-password' ) .done( function( data ) { $pass1.data( 'pw', data ); } ); } ); $cancelButton = $pass1Row.find( 'button.wp-cancel-pw' ); $cancelButton.on( 'click', function () { updateLock = false; // Disable the inputs when hiding to prevent autofill and submission. $pass1.prop( 'disabled', true ); $pass2.prop( 'disabled', true ); // Clear password field and update the UI. $pass1.val( '' ).trigger( 'pwupdate' ); resetToggle( false ); // Hide password controls. $passwordWrapper .hide() .removeClass( 'is-open' ); // Stop an empty password from being submitted as a change. $submitButtons.prop( 'disabled', false ); $generateButton.attr( 'aria-expanded', 'false' ); } ); $pass1Row.closest( 'form' ).on( 'submit', function () { updateLock = false; $pass1.prop( 'disabled', false ); $pass2.prop( 'disabled', false ); $pass2.val( $pass1.val() ); }); } function check_pass_strength() { var pass1 = $('#pass1').val(), strength; $('#pass-strength-result').removeClass('short bad good strong empty'); if ( ! pass1 || '' === pass1.trim() ) { $( '#pass-strength-result' ).addClass( 'empty' ).html( ' ' ); return; } strength = wp.passwordStrength.meter( pass1, wp.passwordStrength.userInputDisallowedList(), pass1 ); switch ( strength ) { case -1: $( '#pass-strength-result' ).addClass( 'bad' ).html( pwsL10n.unknown ); break; case 2: $('#pass-strength-result').addClass('bad').html( pwsL10n.bad ); break; case 3: $('#pass-strength-result').addClass('good').html( pwsL10n.good ); break; case 4: $('#pass-strength-result').addClass('strong').html( pwsL10n.strong ); break; case 5: $('#pass-strength-result').addClass('short').html( pwsL10n.mismatch ); break; default: $('#pass-strength-result').addClass('short').html( pwsL10n.short ); } } function showOrHideWeakPasswordCheckbox() { var passStrengthResult = $('#pass-strength-result'); if ( passStrengthResult.length ) { var passStrength = passStrengthResult[0]; if ( passStrength.className ) { $pass1.addClass( passStrength.className ); if ( $( passStrength ).is( '.short, .bad' ) ) { if ( ! $weakCheckbox.prop( 'checked' ) ) { $submitButtons.prop( 'disabled', true ); } $weakRow.show(); } else { if ( $( passStrength ).is( '.empty' ) ) { $submitButtons.prop( 'disabled', true ); $weakCheckbox.prop( 'checked', false ); } else { $submitButtons.prop( 'disabled', false ); } $weakRow.hide(); } } } } // Debug information copy section. clipboard.on( 'success', function( e ) { var triggerElement = $( e.trigger ), successElement = $( '.success', triggerElement.closest( '.application-password-display' ) ); // Clear the selection and move focus back to the trigger. e.clearSelection(); // Show success visual feedback. clearTimeout( successTimeout ); successElement.removeClass( 'hidden' ); // Hide success visual feedback after 3 seconds since last success. successTimeout = setTimeout( function() { successElement.addClass( 'hidden' ); }, 3000 ); // Handle success audible feedback. wp.a11y.speak( __( 'Application password has been copied to your clipboard.' ) ); } ); $( function() { var $colorpicker, $stylesheet, user_id, current_user_id, select = $( '#display_name' ), current_name = select.val(), greeting = $( '#wp-admin-bar-my-account' ).find( '.display-name' ); $( '#pass1' ).val( '' ).on( 'input' + ' pwupdate', check_pass_strength ); $('#pass-strength-result').show(); $('.color-palette').on( 'click', function() { $(this).siblings('input[name="admin_color"]').prop('checked', true); }); if ( select.length ) { $('#first_name, #last_name, #nickname').on( 'blur.user_profile', function() { var dub = [], inputs = { display_nickname : $('#nickname').val() || '', display_username : $('#user_login').val() || '', display_firstname : $('#first_name').val() || '', display_lastname : $('#last_name').val() || '' }; if ( inputs.display_firstname && inputs.display_lastname ) { inputs.display_firstlast = inputs.display_firstname + ' ' + inputs.display_lastname; inputs.display_lastfirst = inputs.display_lastname + ' ' + inputs.display_firstname; } $.each( $('option', select), function( i, el ){ dub.push( el.value ); }); $.each(inputs, function( id, value ) { if ( ! value ) { return; } var val = value.replace(/<\/?[a-z][^>]*>/gi, ''); if ( inputs[id].length && $.inArray( val, dub ) === -1 ) { dub.push(val); $('

' ); }).fail( function( response ) { $this.siblings( '.notice' ).remove(); $this.before( '' ); }); e.preventDefault(); }); window.generatePassword = generatePassword; // Warn the user if password was generated but not saved. $( window ).on( 'beforeunload', function () { if ( true === updateLock ) { return __( 'Your new password has not been saved.' ); } if ( originalFormContent !== $form.serialize() && ! isSubmitting ) { return __( 'The changes you made will be lost if you navigate away from this page.' ); } }); /* * We need to generate a password as soon as the Reset Password page is loaded, * to avoid double clicking the button to retrieve the first generated password. * See ticket #39638. */ $( function() { if ( $( '.reset-pass-submit' ).length ) { $( '.reset-pass-submit button.wp-generate-pw' ).trigger( 'click' ); } }); })(jQuery); PK1YZb_44site-health.jsnu[/** * Interactions used by the Site Health modules in WordPress. * * @output wp-admin/js/site-health.js */ /* global ajaxurl, ClipboardJS, SiteHealth, wp */ jQuery( function( $ ) { var __ = wp.i18n.__, _n = wp.i18n._n, sprintf = wp.i18n.sprintf, clipboard = new ClipboardJS( '.site-health-copy-buttons .copy-button' ), isStatusTab = $( '.health-check-body.health-check-status-tab' ).length, isDebugTab = $( '.health-check-body.health-check-debug-tab' ).length, pathsSizesSection = $( '#health-check-accordion-block-wp-paths-sizes' ), menuCounterWrapper = $( '#adminmenu .site-health-counter' ), menuCounter = $( '#adminmenu .site-health-counter .count' ), successTimeout; // Debug information copy section. clipboard.on( 'success', function( e ) { var triggerElement = $( e.trigger ), successElement = $( '.success', triggerElement.closest( 'div' ) ); // Clear the selection and move focus back to the trigger. e.clearSelection(); // Show success visual feedback. clearTimeout( successTimeout ); successElement.removeClass( 'hidden' ); // Hide success visual feedback after 3 seconds since last success. successTimeout = setTimeout( function() { successElement.addClass( 'hidden' ); }, 3000 ); // Handle success audible feedback. wp.a11y.speak( __( 'Site information has been copied to your clipboard.' ) ); } ); // Accordion handling in various areas. $( '.health-check-accordion' ).on( 'click', '.health-check-accordion-trigger', function() { var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) ); if ( isExpanded ) { $( this ).attr( 'aria-expanded', 'false' ); $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', true ); } else { $( this ).attr( 'aria-expanded', 'true' ); $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', false ); } } ); // Site Health test handling. $( '.site-health-view-passed' ).on( 'click', function() { var goodIssuesWrapper = $( '#health-check-issues-good' ); goodIssuesWrapper.toggleClass( 'hidden' ); $( this ).attr( 'aria-expanded', ! goodIssuesWrapper.hasClass( 'hidden' ) ); } ); /** * Validates the Site Health test result format. * * @since 5.6.0 * * @param {Object} issue * * @return {boolean} */ function validateIssueData( issue ) { // Expected minimum format of a valid SiteHealth test response. var minimumExpected = { test: 'string', label: 'string', description: 'string' }, passed = true, key, value, subKey, subValue; // If the issue passed is not an object, return a `false` state early. if ( 'object' !== typeof( issue ) ) { return false; } // Loop over expected data and match the data types. for ( key in minimumExpected ) { value = minimumExpected[ key ]; if ( 'object' === typeof( value ) ) { for ( subKey in value ) { subValue = value[ subKey ]; if ( 'undefined' === typeof( issue[ key ] ) || 'undefined' === typeof( issue[ key ][ subKey ] ) || subValue !== typeof( issue[ key ][ subKey ] ) ) { passed = false; } } } else { if ( 'undefined' === typeof( issue[ key ] ) || value !== typeof( issue[ key ] ) ) { passed = false; } } } return passed; } /** * Appends a new issue to the issue list. * * @since 5.2.0 * * @param {Object} issue The issue data. */ function appendIssue( issue ) { var template = wp.template( 'health-check-issue' ), issueWrapper = $( '#health-check-issues-' + issue.status ), heading, count; /* * Validate the issue data format before using it. * If the output is invalid, discard it. */ if ( ! validateIssueData( issue ) ) { return false; } SiteHealth.site_status.issues[ issue.status ]++; count = SiteHealth.site_status.issues[ issue.status ]; // If no test name is supplied, append a placeholder for markup references. if ( typeof issue.test === 'undefined' ) { issue.test = issue.status + count; } if ( 'critical' === issue.status ) { heading = sprintf( _n( '%s critical issue', '%s critical issues', count ), '' + count + '' ); } else if ( 'recommended' === issue.status ) { heading = sprintf( _n( '%s recommended improvement', '%s recommended improvements', count ), '' + count + '' ); } else if ( 'good' === issue.status ) { heading = sprintf( _n( '%s item with no issues detected', '%s items with no issues detected', count ), '' + count + '' ); } if ( heading ) { $( '.site-health-issue-count-title', issueWrapper ).html( heading ); } menuCounter.text( SiteHealth.site_status.issues.critical ); if ( 0 < parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { $( '#health-check-issues-critical' ).removeClass( 'hidden' ); menuCounterWrapper.removeClass( 'count-0' ); } else { menuCounterWrapper.addClass( 'count-0' ); } if ( 0 < parseInt( SiteHealth.site_status.issues.recommended, 0 ) ) { $( '#health-check-issues-recommended' ).removeClass( 'hidden' ); } $( '.issues', '#health-check-issues-' + issue.status ).append( template( issue ) ); } /** * Updates site health status indicator as asynchronous tests are run and returned. * * @since 5.2.0 */ function recalculateProgression() { var r, c, pct; var $progress = $( '.site-health-progress' ); var $wrapper = $progress.closest( '.site-health-progress-wrapper' ); var $progressLabel = $( '.site-health-progress-label', $wrapper ); var $circle = $( '.site-health-progress svg #bar' ); var totalTests = parseInt( SiteHealth.site_status.issues.good, 0 ) + parseInt( SiteHealth.site_status.issues.recommended, 0 ) + ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 ); var failedTests = ( parseInt( SiteHealth.site_status.issues.recommended, 0 ) * 0.5 ) + ( parseInt( SiteHealth.site_status.issues.critical, 0 ) * 1.5 ); var val = 100 - Math.ceil( ( failedTests / totalTests ) * 100 ); if ( 0 === totalTests ) { $progress.addClass( 'hidden' ); return; } $wrapper.removeClass( 'loading' ); r = $circle.attr( 'r' ); c = Math.PI * ( r * 2 ); if ( 0 > val ) { val = 0; } if ( 100 < val ) { val = 100; } pct = ( ( 100 - val ) / 100 ) * c + 'px'; $circle.css( { strokeDashoffset: pct } ); if ( 80 <= val && 0 === parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { $wrapper.addClass( 'green' ).removeClass( 'orange' ); $progressLabel.text( __( 'Good' ) ); announceTestsProgression( 'good' ); } else { $wrapper.addClass( 'orange' ).removeClass( 'green' ); $progressLabel.text( __( 'Should be improved' ) ); announceTestsProgression( 'improvable' ); } if ( isStatusTab ) { $.post( ajaxurl, { 'action': 'health-check-site-status-result', '_wpnonce': SiteHealth.nonce.site_status_result, 'counts': SiteHealth.site_status.issues } ); if ( 100 === val ) { $( '.site-status-all-clear' ).removeClass( 'hide' ); $( '.site-status-has-issues' ).addClass( 'hide' ); } } } /** * Queues the next asynchronous test when we're ready to run it. * * @since 5.2.0 */ function maybeRunNextAsyncTest() { var doCalculation = true; if ( 1 <= SiteHealth.site_status.async.length ) { $.each( SiteHealth.site_status.async, function() { var data = { 'action': 'health-check-' + this.test.replace( '_', '-' ), '_wpnonce': SiteHealth.nonce.site_status }; if ( this.completed ) { return true; } doCalculation = false; this.completed = true; if ( 'undefined' !== typeof( this.has_rest ) && this.has_rest ) { wp.apiRequest( { url: wp.url.addQueryArgs( this.test, { _locale: 'user' } ), headers: this.headers } ) .done( function( response ) { /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ appendIssue( wp.hooks.applyFilters( 'site_status_test_result', response ) ); } ) .fail( function( response ) { var description; if ( 'undefined' !== typeof( response.responseJSON ) && 'undefined' !== typeof( response.responseJSON.message ) ) { description = response.responseJSON.message; } else { description = __( 'No details available' ); } addFailedSiteHealthCheckNotice( this.url, description ); } ) .always( function() { maybeRunNextAsyncTest(); } ); } else { $.post( ajaxurl, data ).done( function( response ) { /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ appendIssue( wp.hooks.applyFilters( 'site_status_test_result', response.data ) ); } ).fail( function( response ) { var description; if ( 'undefined' !== typeof( response.responseJSON ) && 'undefined' !== typeof( response.responseJSON.message ) ) { description = response.responseJSON.message; } else { description = __( 'No details available' ); } addFailedSiteHealthCheckNotice( this.url, description ); } ).always( function() { maybeRunNextAsyncTest(); } ); } return false; } ); } if ( doCalculation ) { recalculateProgression(); } } /** * Add the details of a failed asynchronous test to the list of test results. * * @since 5.6.0 */ function addFailedSiteHealthCheckNotice( url, description ) { var issue; issue = { 'status': 'recommended', 'label': __( 'A test is unavailable' ), 'badge': { 'color': 'red', 'label': __( 'Unavailable' ) }, 'description': '

' + url + '

' + description + '

', 'actions': '' }; /** This filter is documented in wp-admin/includes/class-wp-site-health.php */ appendIssue( wp.hooks.applyFilters( 'site_status_test_result', issue ) ); } if ( 'undefined' !== typeof SiteHealth ) { if ( 0 === SiteHealth.site_status.direct.length && 0 === SiteHealth.site_status.async.length ) { recalculateProgression(); } else { SiteHealth.site_status.issues = { 'good': 0, 'recommended': 0, 'critical': 0 }; } if ( 0 < SiteHealth.site_status.direct.length ) { $.each( SiteHealth.site_status.direct, function() { appendIssue( this ); } ); } if ( 0 < SiteHealth.site_status.async.length ) { maybeRunNextAsyncTest(); } else { recalculateProgression(); } } function getDirectorySizes() { var timestamp = ( new Date().getTime() ); // After 3 seconds announce that we're still waiting for directory sizes. var timeout = window.setTimeout( function() { announceTestsProgression( 'waiting-for-directory-sizes' ); }, 3000 ); wp.apiRequest( { path: '/wp-site-health/v1/directory-sizes' } ).done( function( response ) { updateDirSizes( response || {} ); } ).always( function() { var delay = ( new Date().getTime() ) - timestamp; $( '.health-check-wp-paths-sizes.spinner' ).css( 'visibility', 'hidden' ); if ( delay > 3000 ) { /* * We have announced that we're waiting. * Announce that we're ready after giving at least 3 seconds * for the first announcement to be read out, or the two may collide. */ if ( delay > 6000 ) { delay = 0; } else { delay = 6500 - delay; } window.setTimeout( function() { recalculateProgression(); }, delay ); } else { // Cancel the announcement. window.clearTimeout( timeout ); } $( document ).trigger( 'site-health-info-dirsizes-done' ); } ); } function updateDirSizes( data ) { var copyButton = $( 'button.button.copy-button' ); var clipboardText = copyButton.attr( 'data-clipboard-text' ); $.each( data, function( name, value ) { var text = value.debug || value.size; if ( typeof text !== 'undefined' ) { clipboardText = clipboardText.replace( name + ': loading...', name + ': ' + text ); } } ); copyButton.attr( 'data-clipboard-text', clipboardText ); pathsSizesSection.find( 'td[class]' ).each( function( i, element ) { var td = $( element ); var name = td.attr( 'class' ); if ( data.hasOwnProperty( name ) && data[ name ].size ) { td.text( data[ name ].size ); } } ); } if ( isDebugTab ) { if ( pathsSizesSection.length ) { getDirectorySizes(); } else { recalculateProgression(); } } // Trigger a class toggle when the extended menu button is clicked. $( '.health-check-offscreen-nav-wrapper' ).on( 'click', function() { $( this ).toggleClass( 'visible' ); } ); /** * Announces to assistive technologies the tests progression status. * * @since 6.4.0 * * @param {string} type The type of message to be announced. * * @return {void} */ function announceTestsProgression( type ) { // Only announce the messages in the Site Health pages. if ( 'site-health' !== SiteHealth.screen ) { return; } switch ( type ) { case 'good': wp.a11y.speak( __( 'All site health tests have finished running. Your site is looking good.' ) ); break; case 'improvable': wp.a11y.speak( __( 'All site health tests have finished running. There are items that should be addressed.' ) ); break; case 'waiting-for-directory-sizes': wp.a11y.speak( __( 'Running additional tests... please wait.' ) ); break; default: return; } } } ); PK1YZ|[OתPPinline-edit-post.jsnu[/** * This file contains the functions needed for the inline editing of posts. * * @since 2.7.0 * @output wp-admin/js/inline-edit-post.js */ /* global ajaxurl, typenow, inlineEditPost */ window.wp = window.wp || {}; /** * Manages the quick edit and bulk edit windows for editing posts or pages. * * @namespace inlineEditPost * * @since 2.7.0 * * @type {Object} * * @property {string} type The type of inline editor. * @property {string} what The prefix before the post ID. * */ ( function( $, wp ) { window.inlineEditPost = { /** * Initializes the inline and bulk post editor. * * Binds event handlers to the Escape key to close the inline editor * and to the save and close buttons. Changes DOM to be ready for inline * editing. Adds event handler to bulk edit. * * @since 2.7.0 * * @memberof inlineEditPost * * @return {void} */ init : function(){ var t = this, qeRow = $('#inline-edit'), bulkRow = $('#bulk-edit'); t.type = $('table.widefat').hasClass('pages') ? 'page' : 'post'; // Post ID prefix. t.what = '#post-'; /** * Binds the Escape key to revert the changes and close the quick editor. * * @return {boolean} The result of revert. */ qeRow.on( 'keyup', function(e){ // Revert changes if Escape key is pressed. if ( e.which === 27 ) { return inlineEditPost.revert(); } }); /** * Binds the Escape key to revert the changes and close the bulk editor. * * @return {boolean} The result of revert. */ bulkRow.on( 'keyup', function(e){ // Revert changes if Escape key is pressed. if ( e.which === 27 ) { return inlineEditPost.revert(); } }); /** * Reverts changes and close the quick editor if the cancel button is clicked. * * @return {boolean} The result of revert. */ $( '.cancel', qeRow ).on( 'click', function() { return inlineEditPost.revert(); }); /** * Saves changes in the quick editor if the save(named: update) button is clicked. * * @return {boolean} The result of save. */ $( '.save', qeRow ).on( 'click', function() { return inlineEditPost.save(this); }); /** * If Enter is pressed, and the target is not the cancel button, save the post. * * @return {boolean} The result of save. */ $('td', qeRow).on( 'keydown', function(e){ if ( e.which === 13 && ! $( e.target ).hasClass( 'cancel' ) ) { return inlineEditPost.save(this); } }); /** * Reverts changes and close the bulk editor if the cancel button is clicked. * * @return {boolean} The result of revert. */ $( '.cancel', bulkRow ).on( 'click', function() { return inlineEditPost.revert(); }); /** * Disables the password input field when the private post checkbox is checked. */ $('#inline-edit .inline-edit-private input[value="private"]').on( 'click', function(){ var pw = $('input.inline-edit-password-input'); if ( $(this).prop('checked') ) { pw.val('').prop('disabled', true); } else { pw.prop('disabled', false); } }); /** * Binds click event to the .editinline button which opens the quick editor. */ $( '#the-list' ).on( 'click', '.editinline', function() { $( this ).attr( 'aria-expanded', 'true' ); inlineEditPost.edit( this ); }); // Clone quick edit categories for the bulk editor. var beCategories = $( '#inline-edit fieldset.inline-edit-categories' ).clone(); // Make "id" attributes globally unique. beCategories.find( '*[id]' ).each( function() { this.id = 'bulk-edit-' + this.id; }); $('#bulk-edit').find('fieldset:first').after( beCategories ).siblings( 'fieldset:last' ).prepend( $( '#inline-edit .inline-edit-tags-wrap' ).clone() ); $('select[name="_status"] option[value="future"]', bulkRow).remove(); /** * Adds onclick events to the apply buttons. */ $('#doaction').on( 'click', function(e){ var n, $itemsSelected = $( '#posts-filter .check-column input[type="checkbox"]:checked' ); if ( $itemsSelected.length < 1 ) { return; } t.whichBulkButtonId = $( this ).attr( 'id' ); n = t.whichBulkButtonId.substr( 2 ); if ( 'edit' === $( 'select[name="' + n + '"]' ).val() ) { e.preventDefault(); t.setBulk(); } else if ( $('form#posts-filter tr.inline-editor').length > 0 ) { t.revert(); } }); }, /** * Toggles the quick edit window, hiding it when it's active and showing it when * inactive. * * @since 2.7.0 * * @memberof inlineEditPost * * @param {Object} el Element within a post table row. */ toggle : function(el){ var t = this; $( t.what + t.getId( el ) ).css( 'display' ) === 'none' ? t.revert() : t.edit( el ); }, /** * Creates the bulk editor row to edit multiple posts at once. * * @since 2.7.0 * * @memberof inlineEditPost */ setBulk : function(){ var te = '', type = this.type, c = true; var checkedPosts = $( 'tbody th.check-column input[type="checkbox"]:checked' ); var categories = {}; this.revert(); $( '#bulk-edit td' ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length ); // Insert the editor at the top of the table with an empty row above to maintain zebra striping. $('table.widefat tbody').prepend( $('#bulk-edit') ).prepend(''); $('#bulk-edit').addClass('inline-editor').show(); /** * Create a HTML div with the title and a link(delete-icon) for each selected * post. * * Get the selected posts based on the checked checkboxes in the post table. */ $( 'tbody th.check-column input[type="checkbox"]' ).each( function() { // If the checkbox for a post is selected, add the post to the edit list. if ( $(this).prop('checked') ) { c = false; var id = $( this ).val(), theTitle = $( '#inline_' + id + ' .post_title' ).html() || wp.i18n.__( '(no title)' ), buttonVisuallyHiddenText = wp.i18n.sprintf( /* translators: %s: Post title. */ wp.i18n.__( 'Remove “%s” from Bulk Edit' ), theTitle ); te += '
  • '; } }); // If no checkboxes where checked, just hide the quick/bulk edit rows. if ( c ) { return this.revert(); } // Populate the list of items to bulk edit. $( '#bulk-titles' ).html( '
      ' + te + '
    ' ); // Gather up some statistics on which of these checked posts are in which categories. checkedPosts.each( function() { var id = $( this ).val(); var checked = $( '#category_' + id ).text().split( ',' ); checked.map( function( cid ) { categories[ cid ] || ( categories[ cid ] = 0 ); // Just record that this category is checked. categories[ cid ]++; } ); } ); // Compute initial states. $( '.inline-edit-categories input[name="post_category[]"]' ).each( function() { if ( categories[ $( this ).val() ] == checkedPosts.length ) { // If the number of checked categories matches the number of selected posts, then all posts are in this category. $( this ).prop( 'checked', true ); } else if ( categories[ $( this ).val() ] > 0 ) { // If the number is less than the number of selected posts, then it's indeterminate. $( this ).prop( 'indeterminate', true ); if ( ! $( this ).parent().find( 'input[name="indeterminate_post_category[]"]' ).length ) { // Get the term label text. var label = $( this ).parent().text(); // Set indeterminate states for the backend. Add accessible text for indeterminate inputs. $( this ).after( '' ).attr( 'aria-label', label.trim() + ': ' + wp.i18n.__( 'Some selected posts have this category' ) ); } } } ); $( '.inline-edit-categories input[name="post_category[]"]:indeterminate' ).on( 'change', function() { // Remove accessible label text. Remove the indeterminate flags as there was a specific state change. $( this ).removeAttr( 'aria-label' ).parent().find( 'input[name="indeterminate_post_category[]"]' ).remove(); } ); $( '.inline-edit-save button' ).on( 'click', function() { $( '.inline-edit-categories input[name="post_category[]"]' ).prop( 'indeterminate', false ); } ); /** * Binds on click events to handle the list of items to bulk edit. * * @listens click */ $( '#bulk-titles .ntdelbutton' ).click( function() { var $this = $( this ), id = $this.attr( 'id' ).substr( 1 ), $prev = $this.parent().prev().children( '.ntdelbutton' ), $next = $this.parent().next().children( '.ntdelbutton' ); $( 'input#cb-select-all-1, input#cb-select-all-2' ).prop( 'checked', false ); $( 'table.widefat input[value="' + id + '"]' ).prop( 'checked', false ); $( '#_' + id ).parent().remove(); wp.a11y.speak( wp.i18n.__( 'Item removed.' ), 'assertive' ); // Move focus to a proper place when items are removed. if ( $next.length ) { $next.focus(); } else if ( $prev.length ) { $prev.focus(); } else { $( '#bulk-titles-list' ).remove(); inlineEditPost.revert(); wp.a11y.speak( wp.i18n.__( 'All selected items have been removed. Select new items to use Bulk Actions.' ) ); } }); // Enable auto-complete for tags when editing posts. if ( 'post' === type ) { $( 'tr.inline-editor textarea[data-wp-taxonomy]' ).each( function ( i, element ) { /* * While Quick Edit clones the form each time, Bulk Edit always re-uses * the same form. Let's check if an autocomplete instance already exists. */ if ( $( element ).autocomplete( 'instance' ) ) { // jQuery equivalent of `continue` within an `each()` loop. return; } $( element ).wpTagsSuggest(); } ); } // Set initial focus on the Bulk Edit region. $( '#bulk-edit .inline-edit-wrapper' ).attr( 'tabindex', '-1' ).focus(); // Scrolls to the top of the table where the editor is rendered. $('html, body').animate( { scrollTop: 0 }, 'fast' ); }, /** * Creates a quick edit window for the post that has been clicked. * * @since 2.7.0 * * @memberof inlineEditPost * * @param {number|Object} id The ID of the clicked post or an element within a post * table row. * @return {boolean} Always returns false at the end of execution. */ edit : function(id) { var t = this, fields, editRow, rowData, status, pageOpt, pageLevel, nextPage, pageLoop = true, nextLevel, f, val, pw; t.revert(); if ( typeof(id) === 'object' ) { id = t.getId(id); } fields = ['post_title', 'post_name', 'post_author', '_status', 'jj', 'mm', 'aa', 'hh', 'mn', 'ss', 'post_password', 'post_format', 'menu_order', 'page_template']; if ( t.type === 'page' ) { fields.push('post_parent'); } // Add the new edit row with an extra blank row underneath to maintain zebra striping. editRow = $('#inline-edit').clone(true); $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length ); // Remove the ID from the copied row and let the `for` attribute reference the hidden ID. $( 'td', editRow ).find('#quick-edit-legend').removeAttr('id'); $( 'td', editRow ).find('p[id^="quick-edit-"]').removeAttr('id'); $(t.what+id).removeClass('is-expanded').hide().after(editRow).after(''); // Populate fields in the quick edit window. rowData = $('#inline_'+id); if ( !$(':input[name="post_author"] option[value="' + $('.post_author', rowData).text() + '"]', editRow).val() ) { // The post author no longer has edit capabilities, so we need to add them to the list of authors. $(':input[name="post_author"]', editRow).prepend(''); } if ( $( ':input[name="post_author"] option', editRow ).length === 1 ) { $('label.inline-edit-author', editRow).hide(); } for ( f = 0; f < fields.length; f++ ) { val = $('.'+fields[f], rowData); /** * Replaces the image for a Twemoji(Twitter emoji) with it's alternate text. * * @return {string} Alternate text from the image. */ val.find( 'img' ).replaceWith( function() { return this.alt; } ); val = val.text(); $(':input[name="' + fields[f] + '"]', editRow).val( val ); } if ( $( '.comment_status', rowData ).text() === 'open' ) { $( 'input[name="comment_status"]', editRow ).prop( 'checked', true ); } if ( $( '.ping_status', rowData ).text() === 'open' ) { $( 'input[name="ping_status"]', editRow ).prop( 'checked', true ); } if ( $( '.sticky', rowData ).text() === 'sticky' ) { $( 'input[name="sticky"]', editRow ).prop( 'checked', true ); } /** * Creates the select boxes for the categories. */ $('.post_category', rowData).each(function(){ var taxname, term_ids = $(this).text(); if ( term_ids ) { taxname = $(this).attr('id').replace('_'+id, ''); $('ul.'+taxname+'-checklist :checkbox', editRow).val(term_ids.split(',')); } }); /** * Gets all the taxonomies for live auto-fill suggestions when typing the name * of a tag. */ $('.tags_input', rowData).each(function(){ var terms = $(this), taxname = $(this).attr('id').replace('_' + id, ''), textarea = $('textarea.tax_input_' + taxname, editRow), comma = wp.i18n._x( ',', 'tag delimiter' ).trim(); // Ensure the textarea exists. if ( ! textarea.length ) { return; } terms.find( 'img' ).replaceWith( function() { return this.alt; } ); terms = terms.text(); if ( terms ) { if ( ',' !== comma ) { terms = terms.replace(/,/g, comma); } textarea.val(terms); } textarea.wpTagsSuggest(); }); // Handle the post status. var post_date_string = $(':input[name="aa"]').val() + '-' + $(':input[name="mm"]').val() + '-' + $(':input[name="jj"]').val(); post_date_string += ' ' + $(':input[name="hh"]').val() + ':' + $(':input[name="mn"]').val() + ':' + $(':input[name="ss"]').val(); var post_date = new Date( post_date_string ); status = $('._status', rowData).text(); if ( 'future' !== status && Date.now() > post_date ) { $('select[name="_status"] option[value="future"]', editRow).remove(); } else { $('select[name="_status"] option[value="publish"]', editRow).remove(); } pw = $( '.inline-edit-password-input' ).prop( 'disabled', false ); if ( 'private' === status ) { $('input[name="keep_private"]', editRow).prop('checked', true); pw.val( '' ).prop( 'disabled', true ); } // Remove the current page and children from the parent dropdown. pageOpt = $('select[name="post_parent"] option[value="' + id + '"]', editRow); if ( pageOpt.length > 0 ) { pageLevel = pageOpt[0].className.split('-')[1]; nextPage = pageOpt; while ( pageLoop ) { nextPage = nextPage.next('option'); if ( nextPage.length === 0 ) { break; } nextLevel = nextPage[0].className.split('-')[1]; if ( nextLevel <= pageLevel ) { pageLoop = false; } else { nextPage.remove(); nextPage = pageOpt; } } pageOpt.remove(); } $(editRow).attr('id', 'edit-'+id).addClass('inline-editor').show(); $('.ptitle', editRow).trigger( 'focus' ); return false; }, /** * Saves the changes made in the quick edit window to the post. * Ajax saving is only for Quick Edit and not for bulk edit. * * @since 2.7.0 * * @param {number} id The ID for the post that has been changed. * @return {boolean} False, so the form does not submit when pressing * Enter on a focused field. */ save : function(id) { var params, fields, page = $('.post_status_page').val() || ''; if ( typeof(id) === 'object' ) { id = this.getId(id); } $( 'table.widefat .spinner' ).addClass( 'is-active' ); params = { action: 'inline-save', post_type: typenow, post_ID: id, edit_date: 'true', post_status: page }; fields = $('#edit-'+id).find(':input').serialize(); params = fields + '&' + $.param(params); // Make Ajax request. $.post( ajaxurl, params, function(r) { var $errorNotice = $( '#edit-' + id + ' .inline-edit-save .notice-error' ), $error = $errorNotice.find( '.error' ); $( 'table.widefat .spinner' ).removeClass( 'is-active' ); if (r) { if ( -1 !== r.indexOf( ']*?>/g, '' ); $errorNotice.removeClass( 'hidden' ); $error.html( r ); wp.a11y.speak( $error.text() ); } } else { $errorNotice.removeClass( 'hidden' ); $error.text( wp.i18n.__( 'Error while saving the changes.' ) ); wp.a11y.speak( wp.i18n.__( 'Error while saving the changes.' ) ); } }, 'html'); // Prevent submitting the form when pressing Enter on a focused field. return false; }, /** * Hides and empties the Quick Edit and/or Bulk Edit windows. * * @since 2.7.0 * * @memberof inlineEditPost * * @return {boolean} Always returns false. */ revert : function(){ var $tableWideFat = $( '.widefat' ), id = $( '.inline-editor', $tableWideFat ).attr( 'id' ); if ( id ) { $( '.spinner', $tableWideFat ).removeClass( 'is-active' ); if ( 'bulk-edit' === id ) { // Hide the bulk editor. $( '#bulk-edit', $tableWideFat ).removeClass( 'inline-editor' ).hide().siblings( '.hidden' ).remove(); $('#bulk-titles').empty(); // Store the empty bulk editor in a hidden element. $('#inlineedit').append( $('#bulk-edit') ); // Move focus back to the Bulk Action button that was activated. $( '#' + inlineEditPost.whichBulkButtonId ).trigger( 'focus' ); } else { // Remove both the inline-editor and its hidden tr siblings. $('#'+id).siblings('tr.hidden').addBack().remove(); id = id.substr( id.lastIndexOf('-') + 1 ); // Show the post row and move focus back to the Quick Edit button. $( this.what + id ).show().find( '.editinline' ) .attr( 'aria-expanded', 'false' ) .trigger( 'focus' ); } } return false; }, /** * Gets the ID for a the post that you want to quick edit from the row in the quick * edit table. * * @since 2.7.0 * * @memberof inlineEditPost * * @param {Object} o DOM row object to get the ID for. * @return {string} The post ID extracted from the table row in the object. */ getId : function(o) { var id = $(o).closest('tr').attr('id'), parts = id.split('-'); return parts[parts.length - 1]; } }; $( function() { inlineEditPost.init(); } ); // Show/hide locks on posts. $( function() { // Set the heartbeat interval to 10 seconds. if ( typeof wp !== 'undefined' && wp.heartbeat ) { wp.heartbeat.interval( 10 ); } }).on( 'heartbeat-tick.wp-check-locked-posts', function( e, data ) { var locked = data['wp-check-locked-posts'] || {}; $('#the-list tr').each( function(i, el) { var key = el.id, row = $(el), lock_data, avatar; if ( locked.hasOwnProperty( key ) ) { if ( ! row.hasClass('wp-locked') ) { lock_data = locked[key]; row.find('.column-title .locked-text').text( lock_data.text ); row.find('.check-column checkbox').prop('checked', false); if ( lock_data.avatar_src ) { avatar = $( '', { 'class': 'avatar avatar-18 photo', width: 18, height: 18, alt: '', src: lock_data.avatar_src, srcset: lock_data.avatar_src_2x ? lock_data.avatar_src_2x + ' 2x' : undefined } ); row.find('.column-title .locked-avatar').empty().append( avatar ); } row.addClass('wp-locked'); } } else if ( row.hasClass('wp-locked') ) { row.removeClass( 'wp-locked' ).find( '.locked-info span' ).empty(); } }); }).on( 'heartbeat-send.wp-check-locked-posts', function( e, data ) { var check = []; $('#the-list tr').each( function(i, el) { if ( el.id ) { check.push( el.id ); } }); if ( check.length ) { data['wp-check-locked-posts'] = check; } }); })( jQuery, window.wp ); PK1YZgÌpassword-strength-meter.jsnu[/** * @output wp-admin/js/password-strength-meter.js */ /* global zxcvbn */ window.wp = window.wp || {}; (function($){ var __ = wp.i18n.__, sprintf = wp.i18n.sprintf; /** * Contains functions to determine the password strength. * * @since 3.7.0 * * @namespace */ wp.passwordStrength = { /** * Determines the strength of a given password. * * Compares first password to the password confirmation. * * @since 3.7.0 * * @param {string} password1 The subject password. * @param {Array} disallowedList An array of words that will lower the entropy of * the password. * @param {string} password2 The password confirmation. * * @return {number} The password strength score. */ meter : function( password1, disallowedList, password2 ) { if ( ! Array.isArray( disallowedList ) ) disallowedList = [ disallowedList.toString() ]; if (password1 != password2 && password2 && password2.length > 0) return 5; if ( 'undefined' === typeof window.zxcvbn ) { // Password strength unknown. return -1; } var result = zxcvbn( password1, disallowedList ); return result.score; }, /** * Builds an array of words that should be penalized. * * Certain words need to be penalized because it would lower the entropy of a * password if they were used. The disallowedList is based on user input fields such * as username, first name, email etc. * * @since 3.7.0 * @deprecated 5.5.0 Use {@see 'userInputDisallowedList()'} instead. * * @return {string[]} The array of words to be disallowed. */ userInputBlacklist : function() { window.console.log( sprintf( /* translators: 1: Deprecated function name, 2: Version number, 3: Alternative function name. */ __( '%1$s is deprecated since version %2$s! Use %3$s instead. Please consider writing more inclusive code.' ), 'wp.passwordStrength.userInputBlacklist()', '5.5.0', 'wp.passwordStrength.userInputDisallowedList()' ) ); return wp.passwordStrength.userInputDisallowedList(); }, /** * Builds an array of words that should be penalized. * * Certain words need to be penalized because it would lower the entropy of a * password if they were used. The disallowed list is based on user input fields such * as username, first name, email etc. * * @since 5.5.0 * * @return {string[]} The array of words to be disallowed. */ userInputDisallowedList : function() { var i, userInputFieldsLength, rawValuesLength, currentField, rawValues = [], disallowedList = [], userInputFields = [ 'user_login', 'first_name', 'last_name', 'nickname', 'display_name', 'email', 'url', 'description', 'weblog_title', 'admin_email' ]; // Collect all the strings we want to disallow. rawValues.push( document.title ); rawValues.push( document.URL ); userInputFieldsLength = userInputFields.length; for ( i = 0; i < userInputFieldsLength; i++ ) { currentField = $( '#' + userInputFields[ i ] ); if ( 0 === currentField.length ) { continue; } rawValues.push( currentField[0].defaultValue ); rawValues.push( currentField.val() ); } /* * Strip out non-alphanumeric characters and convert each word to an * individual entry. */ rawValuesLength = rawValues.length; for ( i = 0; i < rawValuesLength; i++ ) { if ( rawValues[ i ] ) { disallowedList = disallowedList.concat( rawValues[ i ].replace( /\W/g, ' ' ).split( ' ' ) ); } } /* * Remove empty values, short words and duplicates. Short words are likely to * cause many false positives. */ disallowedList = $.grep( disallowedList, function( value, key ) { if ( '' === value || 4 > value.length ) { return false; } return $.inArray( value, disallowedList ) === key; }); return disallowedList; } }; // Backward compatibility. /** * Password strength meter function. * * @since 2.5.0 * @deprecated 3.7.0 Use wp.passwordStrength.meter instead. * * @global * * @type {wp.passwordStrength.meter} */ window.passwordStrength = wp.passwordStrength.meter; })(jQuery); PK1YZaaccordion.min.jsnu[/*! This file is auto-generated */ !function(s){s(function(){s(".accordion-container").on("click",".accordion-section-title button",function(){var n,o,e,a,t,i;n=s(this),o=n.closest(".accordion-section"),e=o.closest(".accordion-container"),a=e.find(".open"),t=a.find("[aria-expanded]").first(),i=o.find(".accordion-section-content"),o.hasClass("cannot-expand")||(e.addClass("opening"),o.hasClass("open")?(o.toggleClass("open"),i.toggle(!0).slideToggle(150)):(t.attr("aria-expanded","false"),a.removeClass("open"),a.find(".accordion-section-content").show().slideUp(150),i.toggle(!1).slideToggle(150),o.toggleClass("open")),setTimeout(function(){e.removeClass("opening")},150),n&&n.attr("aria-expanded",String("false"===n.attr("aria-expanded"))))})})}(jQuery);PK1YZDxfn.jsnu[/** * Generates the XHTML Friends Network 'rel' string from the inputs. * * @deprecated 3.5.0 * @output wp-admin/js/xfn.js */ jQuery( function( $ ) { $( '#link_rel' ).prop( 'readonly', true ); $( '#linkxfndiv input' ).on( 'click keyup', function() { var isMe = $( '#me' ).is( ':checked' ), inputs = ''; $( 'input.valinp' ).each( function() { if ( isMe ) { $( this ).prop( 'disabled', true ).parent().addClass( 'disabled' ); } else { $( this ).removeAttr( 'disabled' ).parent().removeClass( 'disabled' ); if ( $( this ).is( ':checked' ) && $( this ).val() !== '') { inputs += $( this ).val() + ' '; } } }); $( '#link_rel' ).val( ( isMe ) ? 'me' : inputs.substr( 0,inputs.length - 1 ) ); }); }); PK1YZ;;password-toggle.jsnu[/** * Adds functionality for password visibility buttons to toggle between text and password input types. * * @since 6.3.0 * @output wp-admin/js/password-toggle.js */ ( function () { var toggleElements, status, input, icon, label, __ = wp.i18n.__; toggleElements = document.querySelectorAll( '.pwd-toggle' ); toggleElements.forEach( function (toggle) { toggle.classList.remove( 'hide-if-no-js' ); toggle.addEventListener( 'click', togglePassword ); } ); function togglePassword() { status = this.getAttribute( 'data-toggle' ); input = this.parentElement.children.namedItem( 'pwd' ); icon = this.getElementsByClassName( 'dashicons' )[ 0 ]; label = this.getElementsByClassName( 'text' )[ 0 ]; if ( 0 === parseInt( status, 10 ) ) { this.setAttribute( 'data-toggle', 1 ); this.setAttribute( 'aria-label', __( 'Hide password' ) ); input.setAttribute( 'type', 'text' ); label.innerHTML = __( 'Hide' ); icon.classList.remove( 'dashicons-visibility' ); icon.classList.add( 'dashicons-hidden' ); } else { this.setAttribute( 'data-toggle', 0 ); this.setAttribute( 'aria-label', __( 'Show password' ) ); input.setAttribute( 'type', 'password' ); label.innerHTML = __( 'Show' ); icon.classList.remove( 'dashicons-hidden' ); icon.classList.add( 'dashicons-visibility' ); } } } )(); PK1YZtheme.jsnu[/** * @output wp-admin/js/theme.js */ /* global _wpThemeSettings, confirm, tb_position */ window.wp = window.wp || {}; ( function($) { // Set up our namespace... var themes, l10n; themes = wp.themes = wp.themes || {}; // Store the theme data and settings for organized and quick access. // themes.data.settings, themes.data.themes, themes.data.l10n. themes.data = _wpThemeSettings; l10n = themes.data.l10n; // Shortcut for isInstall check. themes.isInstall = !! themes.data.settings.isInstall; // Setup app structure. _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template }); themes.Model = Backbone.Model.extend({ // Adds attributes to the default data coming through the .org themes api. // Map `id` to `slug` for shared code. initialize: function() { var description; if ( this.get( 'slug' ) ) { // If the theme is already installed, set an attribute. if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) { this.set({ installed: true }); } // If the theme is active, set an attribute. if ( themes.data.activeTheme === this.get( 'slug' ) ) { this.set({ active: true }); } } // Set the attributes. this.set({ // `slug` is for installation, `id` is for existing. id: this.get( 'slug' ) || this.get( 'id' ) }); // Map `section.description` to `description` // as the API sometimes returns it differently. if ( this.has( 'sections' ) ) { description = this.get( 'sections' ).description; this.set({ description: description }); } } }); // Main view controller for themes.php. // Unifies and renders all available views. themes.view.Appearance = wp.Backbone.View.extend({ el: '#wpbody-content .wrap .theme-browser', window: $( window ), // Pagination instance. page: 0, // Sets up a throttler for binding to 'scroll'. initialize: function( options ) { // Scroller checks how far the scroll position is. _.bindAll( this, 'scroller' ); this.SearchView = options.SearchView ? options.SearchView : themes.view.Search; // Bind to the scroll event and throttle // the results from this.scroller. this.window.on( 'scroll', _.throttle( this.scroller, 300 ) ); }, // Main render control. render: function() { // Setup the main theme view // with the current theme collection. this.view = new themes.view.Themes({ collection: this.collection, parent: this }); // Render search form. this.search(); this.$el.removeClass( 'search-loading' ); // Render and append. this.view.render(); this.$el.empty().append( this.view.el ).addClass( 'rendered' ); }, // Defines search element container. searchContainer: $( '.search-form' ), // Search input and view // for current theme collection. search: function() { var view, self = this; // Don't render the search if there is only one theme. if ( themes.data.themes.length === 1 ) { return; } view = new this.SearchView({ collection: self.collection, parent: this }); self.SearchView = view; // Render and append after screen title. view.render(); this.searchContainer .find( '.search-box' ) .append( $.parseHTML( '' ) ) .append( view.el ); this.searchContainer.on( 'submit', function( event ) { event.preventDefault(); }); }, // Checks when the user gets close to the bottom // of the mage and triggers a theme:scroll event. scroller: function() { var self = this, bottom, threshold; bottom = this.window.scrollTop() + self.window.height(); threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height(); threshold = Math.round( threshold * 0.9 ); if ( bottom > threshold ) { this.trigger( 'theme:scroll' ); } } }); // Set up the Collection for our theme data. // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ... themes.Collection = Backbone.Collection.extend({ model: themes.Model, // Search terms. terms: '', // Controls searching on the current theme collection // and triggers an update event. doSearch: function( value ) { // Don't do anything if we've already done this search. // Useful because the Search handler fires multiple times per keystroke. if ( this.terms === value ) { return; } // Updates terms with the value passed. this.terms = value; // If we have terms, run a search... if ( this.terms.length > 0 ) { this.search( this.terms ); } // If search is blank, show all themes. // Useful for resetting the views when you clean the input. if ( this.terms === '' ) { this.reset( themes.data.themes ); $( 'body' ).removeClass( 'no-results' ); } // Trigger a 'themes:update' event. this.trigger( 'themes:update' ); }, /** * Performs a search within the collection. * * @uses RegExp */ search: function( term ) { var match, results, haystack, name, description, author; // Start with a full collection. this.reset( themes.data.themes, { silent: true } ); // Trim the term. term = term.trim(); // Escape the term string for RegExp meta characters. term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); // Consider spaces as word delimiters and match the whole string // so matching terms can be combined. term = term.replace( / /g, ')(?=.*' ); match = new RegExp( '^(?=.*' + term + ').+', 'i' ); // Find results. // _.filter() and .test(). results = this.filter( function( data ) { name = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' ); description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' ); author = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' ); haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] ); if ( match.test( data.get( 'author' ) ) && term.length > 2 ) { data.set( 'displayAuthor', true ); } return match.test( haystack ); }); if ( results.length === 0 ) { this.trigger( 'query:empty' ); } else { $( 'body' ).removeClass( 'no-results' ); } this.reset( results ); }, // Paginates the collection with a helper method // that slices the collection. paginate: function( instance ) { var collection = this; instance = instance || 0; // Themes per instance are set at 20. collection = _( collection.rest( 20 * instance ) ); collection = _( collection.first( 20 ) ); return collection; }, count: false, /* * Handles requests for more themes and caches results. * * * When we are missing a cache object we fire an apiCall() * which triggers events of `query:success` or `query:fail`. */ query: function( request ) { /** * @static * @type Array */ var queries = this.queries, self = this, query, isPaginated, count; // Store current query request args // for later use with the event `theme:end`. this.currentQuery.request = request; // Search the query cache for matches. query = _.find( queries, function( query ) { return _.isEqual( query.request, request ); }); // If the request matches the stored currentQuery.request // it means we have a paginated request. isPaginated = _.has( request, 'page' ); // Reset the internal api page counter for non-paginated queries. if ( ! isPaginated ) { this.currentQuery.page = 1; } // Otherwise, send a new API call and add it to the cache. if ( ! query && ! isPaginated ) { query = this.apiCall( request ).done( function( data ) { // Update the collection with the queried data. if ( data.themes ) { self.reset( data.themes ); count = data.info.results; // Store the results and the query request. queries.push( { themes: data.themes, request: request, total: count } ); } // Trigger a collection refresh event // and a `query:success` event with a `count` argument. self.trigger( 'themes:update' ); self.trigger( 'query:success', count ); if ( data.themes && data.themes.length === 0 ) { self.trigger( 'query:empty' ); } }).fail( function() { self.trigger( 'query:fail' ); }); } else { // If it's a paginated request we need to fetch more themes... if ( isPaginated ) { return this.apiCall( request, isPaginated ).done( function( data ) { // Add the new themes to the current collection. // @todo Update counter. self.add( data.themes ); self.trigger( 'query:success' ); // We are done loading themes for now. self.loadingThemes = false; }).fail( function() { self.trigger( 'query:fail' ); }); } if ( query.themes.length === 0 ) { self.trigger( 'query:empty' ); } else { $( 'body' ).removeClass( 'no-results' ); } // Only trigger an update event since we already have the themes // on our cached object. if ( _.isNumber( query.total ) ) { this.count = query.total; } this.reset( query.themes ); if ( ! query.total ) { this.count = this.length; } this.trigger( 'themes:update' ); this.trigger( 'query:success', this.count ); } }, // Local cache array for API queries. queries: [], // Keep track of current query so we can handle pagination. currentQuery: { page: 1, request: {} }, // Send request to api.wordpress.org/themes. apiCall: function( request, paginated ) { return wp.ajax.send( 'query-themes', { data: { // Request data. request: _.extend({ per_page: 100 }, request) }, beforeSend: function() { if ( ! paginated ) { // Spin it. $( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' ); } } }); }, // Static status controller for when we are loading themes. loadingThemes: false }); // This is the view that controls each theme item // that will be displayed on the screen. themes.view.Theme = wp.Backbone.View.extend({ // Wrap theme data on a div.theme element. className: 'theme', // Reflects which theme view we have. // 'grid' (default) or 'detail'. state: 'grid', // The HTML template for each element to be rendered. html: themes.template( 'theme' ), events: { 'click': themes.isInstall ? 'preview': 'expand', 'keydown': themes.isInstall ? 'preview': 'expand', 'touchend': themes.isInstall ? 'preview': 'expand', 'keyup': 'addFocus', 'touchmove': 'preventExpand', 'click .theme-install': 'installTheme', 'click .update-message': 'updateTheme' }, touchDrag: false, initialize: function() { this.model.on( 'change', this.render, this ); }, render: function() { var data = this.model.toJSON(); // Render themes using the html template. this.$el.html( this.html( data ) ).attr( 'data-slug', data.id ); // Renders active theme styles. this.activeTheme(); if ( this.model.get( 'displayAuthor' ) ) { this.$el.addClass( 'display-author' ); } }, // Adds a class to the currently active theme // and to the overlay in detailed view mode. activeTheme: function() { if ( this.model.get( 'active' ) ) { this.$el.addClass( 'active' ); } }, // Add class of focus to the theme we are focused on. addFocus: function() { var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme'); $('.theme.focus').removeClass('focus'); $themeToFocus.addClass('focus'); }, // Single theme overlay screen. // It's shown when clicking a theme. expand: function( event ) { var self = this; event = event || window.event; // 'Enter' and 'Space' keys expand the details view when a theme is :focused. if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { return; } // Bail if the user scrolled on a touch device. if ( this.touchDrag === true ) { return this.touchDrag = false; } // Prevent the modal from showing when the user clicks // one of the direct action buttons. if ( $( event.target ).is( '.theme-actions a' ) ) { return; } // Prevent the modal from showing when the user clicks one of the direct action buttons. if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) { return; } // Set focused theme to current element. themes.focusedTheme = this.$el; this.trigger( 'theme:expand', self.model.cid ); }, preventExpand: function() { this.touchDrag = true; }, preview: function( event ) { var self = this, current, preview; event = event || window.event; // Bail if the user scrolled on a touch device. if ( this.touchDrag === true ) { return this.touchDrag = false; } // Allow direct link path to installing a theme. if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) { return; } // 'Enter' and 'Space' keys expand the details view when a theme is :focused. if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { return; } // Pressing Enter while focused on the buttons shouldn't open the preview. if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) { return; } event.preventDefault(); event = event || window.event; // Set focus to current theme. themes.focusedTheme = this.$el; // Construct a new Preview view. themes.preview = preview = new themes.view.Preview({ model: this.model }); // Render the view and append it. preview.render(); this.setNavButtonsState(); // Hide previous/next navigation if there is only one theme. if ( this.model.collection.length === 1 ) { preview.$el.addClass( 'no-navigation' ); } else { preview.$el.removeClass( 'no-navigation' ); } // Append preview. $( 'div.wrap' ).append( preview.el ); // Listen to our preview object // for `theme:next` and `theme:previous` events. this.listenTo( preview, 'theme:next', function() { // Keep local track of current theme model. current = self.model; // If we have ventured away from current model update the current model position. if ( ! _.isUndefined( self.current ) ) { current = self.current; } // Get next theme model. self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 ); // If we have no more themes, bail. if ( _.isUndefined( self.current ) ) { self.options.parent.parent.trigger( 'theme:end' ); return self.current = current; } preview.model = self.current; // Render and append. preview.render(); this.setNavButtonsState(); $( '.next-theme' ).trigger( 'focus' ); }) .listenTo( preview, 'theme:previous', function() { // Keep track of current theme model. current = self.model; // Bail early if we are at the beginning of the collection. if ( self.model.collection.indexOf( self.current ) === 0 ) { return; } // If we have ventured away from current model update the current model position. if ( ! _.isUndefined( self.current ) ) { current = self.current; } // Get previous theme model. self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 ); // If we have no more themes, bail. if ( _.isUndefined( self.current ) ) { return; } preview.model = self.current; // Render and append. preview.render(); this.setNavButtonsState(); $( '.previous-theme' ).trigger( 'focus' ); }); this.listenTo( preview, 'preview:close', function() { self.current = self.model; }); }, // Handles .disabled classes for previous/next buttons in theme installer preview. setNavButtonsState: function() { var $themeInstaller = $( '.theme-install-overlay' ), current = _.isUndefined( this.current ) ? this.model : this.current, previousThemeButton = $themeInstaller.find( '.previous-theme' ), nextThemeButton = $themeInstaller.find( '.next-theme' ); // Disable previous at the zero position. if ( 0 === this.model.collection.indexOf( current ) ) { previousThemeButton .addClass( 'disabled' ) .prop( 'disabled', true ); nextThemeButton.trigger( 'focus' ); } // Disable next if the next model is undefined. if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) { nextThemeButton .addClass( 'disabled' ) .prop( 'disabled', true ); previousThemeButton.trigger( 'focus' ); } }, installTheme: function( event ) { var _this = this; event.preventDefault(); wp.updates.maybeRequestFilesystemCredentials( event ); $( document ).on( 'wp-theme-install-success', function( event, response ) { if ( _this.model.get( 'id' ) === response.slug ) { _this.model.set( { 'installed': true } ); } if ( response.blockTheme ) { _this.model.set( { 'block_theme': true } ); } } ); wp.updates.installTheme( { slug: $( event.target ).data( 'slug' ) } ); }, updateTheme: function( event ) { var _this = this; if ( ! this.model.get( 'hasPackage' ) ) { return; } event.preventDefault(); wp.updates.maybeRequestFilesystemCredentials( event ); $( document ).on( 'wp-theme-update-success', function( event, response ) { _this.model.off( 'change', _this.render, _this ); if ( _this.model.get( 'id' ) === response.slug ) { _this.model.set( { hasUpdate: false, version: response.newVersion } ); } _this.model.on( 'change', _this.render, _this ); } ); wp.updates.updateTheme( { slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' ) } ); } }); // Theme Details view. // Sets up a modal overlay with the expanded theme data. themes.view.Details = wp.Backbone.View.extend({ // Wrap theme data on a div.theme element. className: 'theme-overlay', events: { 'click': 'collapse', 'click .delete-theme': 'deleteTheme', 'click .left': 'previousTheme', 'click .right': 'nextTheme', 'click #update-theme': 'updateTheme', 'click .toggle-auto-update': 'autoupdateState' }, // The HTML template for the theme overlay. html: themes.template( 'theme-single' ), render: function() { var data = this.model.toJSON(); this.$el.html( this.html( data ) ); // Renders active theme styles. this.activeTheme(); // Set up navigation events. this.navigation(); // Checks screenshot size. this.screenshotCheck( this.$el ); // Contain "tabbing" inside the overlay. this.containFocus( this.$el ); }, // Adds a class to the currently active theme // and to the overlay in detailed view mode. activeTheme: function() { // Check the model has the active property. this.$el.toggleClass( 'active', this.model.get( 'active' ) ); }, // Set initial focus and constrain tabbing within the theme browser modal. containFocus: function( $el ) { // Set initial focus on the primary action control. _.delay( function() { $( '.theme-overlay' ).trigger( 'focus' ); }, 100 ); // Constrain tabbing within the modal. $el.on( 'keydown.wp-themes', function( event ) { var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(), $lastFocusable = $el.find( '.theme-actions a:visible' ).last(); // Check for the Tab key. if ( 9 === event.which ) { if ( $firstFocusable[0] === event.target && event.shiftKey ) { $lastFocusable.trigger( 'focus' ); event.preventDefault(); } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) { $firstFocusable.trigger( 'focus' ); event.preventDefault(); } } }); }, // Single theme overlay screen. // It's shown when clicking a theme. collapse: function( event ) { var self = this, scroll; event = event || window.event; // Prevent collapsing detailed view when there is only one theme available. if ( themes.data.themes.length === 1 ) { return; } // Detect if the click is inside the overlay and don't close it // unless the target was the div.back button. if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) { // Add a temporary closing class while overlay fades out. $( 'body' ).addClass( 'closing-overlay' ); // With a quick fade out animation. this.$el.fadeOut( 130, function() { // Clicking outside the modal box closes the overlay. $( 'body' ).removeClass( 'closing-overlay' ); // Handle event cleanup. self.closeOverlay(); // Get scroll position to avoid jumping to the top. scroll = document.body.scrollTop; // Clean the URL structure. themes.router.navigate( themes.router.baseUrl( '' ) ); // Restore scroll position. document.body.scrollTop = scroll; // Return focus to the theme div. if ( themes.focusedTheme ) { themes.focusedTheme.find('.more-details').trigger( 'focus' ); } }); } }, // Handles .disabled classes for next/previous buttons. navigation: function() { // Disable Left/Right when at the start or end of the collection. if ( this.model.cid === this.model.collection.at(0).cid ) { this.$el.find( '.left' ) .addClass( 'disabled' ) .prop( 'disabled', true ); } if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) { this.$el.find( '.right' ) .addClass( 'disabled' ) .prop( 'disabled', true ); } }, // Performs the actions to effectively close // the theme details overlay. closeOverlay: function() { $( 'body' ).removeClass( 'modal-open' ); this.remove(); this.unbind(); this.trigger( 'theme:collapse' ); }, // Set state of the auto-update settings link after it has been changed and saved. autoupdateState: function() { var callback, _this = this; // Support concurrent clicks in different Theme Details overlays. callback = function( event, data ) { var autoupdate; if ( _this.model.get( 'id' ) === data.asset ) { autoupdate = _this.model.get( 'autoupdate' ); autoupdate.enabled = 'enable' === data.state; _this.model.set( { autoupdate: autoupdate } ); $( document ).off( 'wp-auto-update-setting-changed', callback ); } }; // Triggered in updates.js $( document ).on( 'wp-auto-update-setting-changed', callback ); }, updateTheme: function( event ) { var _this = this; event.preventDefault(); wp.updates.maybeRequestFilesystemCredentials( event ); $( document ).on( 'wp-theme-update-success', function( event, response ) { if ( _this.model.get( 'id' ) === response.slug ) { _this.model.set( { hasUpdate: false, version: response.newVersion } ); } _this.render(); } ); wp.updates.updateTheme( { slug: $( event.target ).data( 'slug' ) } ); }, deleteTheme: function( event ) { var _this = this, _collection = _this.model.collection, _themes = themes; event.preventDefault(); // Confirmation dialog for deleting a theme. if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); $( document ).one( 'wp-theme-delete-success', function( event, response ) { _this.$el.find( '.close' ).trigger( 'click' ); $( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() { $( this ).remove(); _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) ); $( '.wp-filter-search' ).val( '' ); _collection.doSearch( '' ); _collection.remove( _this.model ); _collection.trigger( 'themes:update' ); } ); } ); wp.updates.deleteTheme( { slug: this.model.get( 'id' ) } ); }, nextTheme: function() { var self = this; self.trigger( 'theme:next', self.model.cid ); return false; }, previousTheme: function() { var self = this; self.trigger( 'theme:previous', self.model.cid ); return false; }, // Checks if the theme screenshot is the old 300px width version // and adds a corresponding class if it's true. screenshotCheck: function( el ) { var screenshot, image; screenshot = el.find( '.screenshot img' ); image = new Image(); image.src = screenshot.attr( 'src' ); // Width check. if ( image.width && image.width <= 300 ) { el.addClass( 'small-screenshot' ); } } }); // Theme Preview view. // Sets up a modal overlay with the expanded theme data. themes.view.Preview = themes.view.Details.extend({ className: 'wp-full-overlay expanded', el: '.theme-install-overlay', events: { 'click .close-full-overlay': 'close', 'click .collapse-sidebar': 'collapse', 'click .devices button': 'previewDevice', 'click .previous-theme': 'previousTheme', 'click .next-theme': 'nextTheme', 'keyup': 'keyEvent', 'click .theme-install': 'installTheme' }, // The HTML template for the theme preview. html: themes.template( 'theme-preview' ), render: function() { var self = this, currentPreviewDevice, data = this.model.toJSON(), $body = $( document.body ); $body.attr( 'aria-busy', 'true' ); this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) ); currentPreviewDevice = this.$el.data( 'current-preview-device' ); if ( currentPreviewDevice ) { self.togglePreviewDeviceButtons( currentPreviewDevice ); } themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } ); this.$el.fadeIn( 200, function() { $body.addClass( 'theme-installer-active full-overlay-active' ); }); this.$el.find( 'iframe' ).one( 'load', function() { self.iframeLoaded(); }); }, iframeLoaded: function() { this.$el.addClass( 'iframe-ready' ); $( document.body ).attr( 'aria-busy', 'false' ); }, close: function() { this.$el.fadeOut( 200, function() { $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' ); // Return focus to the theme div. if ( themes.focusedTheme ) { themes.focusedTheme.find('.more-details').trigger( 'focus' ); } }).removeClass( 'iframe-ready' ); // Restore the previous browse tab if available. if ( themes.router.selectedTab ) { themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) ); themes.router.selectedTab = false; } else { themes.router.navigate( themes.router.baseUrl( '' ) ); } this.trigger( 'preview:close' ); this.undelegateEvents(); this.unbind(); return false; }, collapse: function( event ) { var $button = $( event.currentTarget ); if ( 'true' === $button.attr( 'aria-expanded' ) ) { $button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar }); } else { $button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar }); } this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); return false; }, previewDevice: function( event ) { var device = $( event.currentTarget ).data( 'device' ); this.$el .removeClass( 'preview-desktop preview-tablet preview-mobile' ) .addClass( 'preview-' + device ) .data( 'current-preview-device', device ); this.togglePreviewDeviceButtons( device ); }, togglePreviewDeviceButtons: function( newDevice ) { var $devices = $( '.wp-full-overlay-footer .devices' ); $devices.find( 'button' ) .removeClass( 'active' ) .attr( 'aria-pressed', false ); $devices.find( 'button.preview-' + newDevice ) .addClass( 'active' ) .attr( 'aria-pressed', true ); }, keyEvent: function( event ) { // The escape key closes the preview. if ( event.keyCode === 27 ) { this.undelegateEvents(); this.close(); } // The right arrow key, next theme. if ( event.keyCode === 39 ) { _.once( this.nextTheme() ); } // The left arrow key, previous theme. if ( event.keyCode === 37 ) { this.previousTheme(); } }, installTheme: function( event ) { var _this = this, $target = $( event.target ); event.preventDefault(); if ( $target.hasClass( 'disabled' ) ) { return; } wp.updates.maybeRequestFilesystemCredentials( event ); $( document ).on( 'wp-theme-install-success', function() { _this.model.set( { 'installed': true } ); } ); wp.updates.installTheme( { slug: $target.data( 'slug' ) } ); } }); // Controls the rendering of div.themes, // a wrapper that will hold all the theme elements. themes.view.Themes = wp.Backbone.View.extend({ className: 'themes wp-clearfix', $overlay: $( 'div.theme-overlay' ), // Number to keep track of scroll position // while in theme-overlay mode. index: 0, // The theme count element. count: $( '.wrap .theme-count' ), // The live themes count. liveThemeCount: 0, initialize: function( options ) { var self = this; // Set up parent. this.parent = options.parent; // Set current view to [grid]. this.setView( 'grid' ); // Move the active theme to the beginning of the collection. self.currentTheme(); // When the collection is updated by user input... this.listenTo( self.collection, 'themes:update', function() { self.parent.page = 0; self.currentTheme(); self.render( this ); } ); // Update theme count to full result set when available. this.listenTo( self.collection, 'query:success', function( count ) { if ( _.isNumber( count ) ) { self.count.text( count ); self.announceSearchResults( count ); } else { self.count.text( self.collection.length ); self.announceSearchResults( self.collection.length ); } }); this.listenTo( self.collection, 'query:empty', function() { $( 'body' ).addClass( 'no-results' ); }); this.listenTo( this.parent, 'theme:scroll', function() { self.renderThemes( self.parent.page ); }); this.listenTo( this.parent, 'theme:close', function() { if ( self.overlay ) { self.overlay.closeOverlay(); } } ); // Bind keyboard events. $( 'body' ).on( 'keyup', function( event ) { if ( ! self.overlay ) { return; } // Bail if the filesystem credentials dialog is shown. if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) { return; } // Pressing the right arrow key fires a theme:next event. if ( event.keyCode === 39 ) { self.overlay.nextTheme(); } // Pressing the left arrow key fires a theme:previous event. if ( event.keyCode === 37 ) { self.overlay.previousTheme(); } // Pressing the escape key fires a theme:collapse event. if ( event.keyCode === 27 ) { self.overlay.collapse( event ); } }); }, // Manages rendering of theme pages // and keeping theme count in sync. render: function() { // Clear the DOM, please. this.$el.empty(); // If the user doesn't have switch capabilities or there is only one theme // in the collection, render the detailed view of the active theme. if ( themes.data.themes.length === 1 ) { // Constructs the view. this.singleTheme = new themes.view.Details({ model: this.collection.models[0] }); // Render and apply a 'single-theme' class to our container. this.singleTheme.render(); this.$el.addClass( 'single-theme' ); this.$el.append( this.singleTheme.el ); } // Generate the themes using page instance // while checking the collection has items. if ( this.options.collection.size() > 0 ) { this.renderThemes( this.parent.page ); } // Display a live theme count for the collection. this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length; this.count.text( this.liveThemeCount ); /* * In the theme installer the themes count is already announced * because `announceSearchResults` is called on `query:success`. */ if ( ! themes.isInstall ) { this.announceSearchResults( this.liveThemeCount ); } }, // Iterates through each instance of the collection // and renders each theme module. renderThemes: function( page ) { var self = this; self.instance = self.collection.paginate( page ); // If we have no more themes, bail. if ( self.instance.size() === 0 ) { // Fire a no-more-themes event. this.parent.trigger( 'theme:end' ); return; } // Make sure the add-new stays at the end. if ( ! themes.isInstall && page >= 1 ) { $( '.add-new-theme' ).remove(); } // Loop through the themes and setup each theme view. self.instance.each( function( theme ) { self.theme = new themes.view.Theme({ model: theme, parent: self }); // Render the views... self.theme.render(); // ...and append them to div.themes. self.$el.append( self.theme.el ); // Binds to theme:expand to show the modal box // with the theme details. self.listenTo( self.theme, 'theme:expand', self.expand, self ); }); // 'Add new theme' element shown at the end of the grid. if ( ! themes.isInstall && themes.data.settings.canInstall ) { this.$el.append( '' ); } this.parent.page++; }, // Grabs current theme and puts it at the beginning of the collection. currentTheme: function() { var self = this, current; current = self.collection.findWhere({ active: true }); // Move the active theme to the beginning of the collection. if ( current ) { self.collection.remove( current ); self.collection.add( current, { at:0 } ); } }, // Sets current view. setView: function( view ) { return view; }, // Renders the overlay with the ThemeDetails view. // Uses the current model data. expand: function( id ) { var self = this, $card, $modal; // Set the current theme model. this.model = self.collection.get( id ); // Trigger a route update for the current model. themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) ); // Sets this.view to 'detail'. this.setView( 'detail' ); $( 'body' ).addClass( 'modal-open' ); // Set up the theme details view. this.overlay = new themes.view.Details({ model: self.model }); this.overlay.render(); if ( this.model.get( 'hasUpdate' ) ) { $card = $( '[data-slug="' + this.model.id + '"]' ); $modal = $( this.overlay.el ); if ( $card.find( '.updating-message' ).length ) { $modal.find( '.notice-warning h3' ).remove(); $modal.find( '.notice-warning' ) .removeClass( 'notice-large' ) .addClass( 'updating-message' ) .find( 'p' ).text( wp.updates.l10n.updating ); } else if ( $card.find( '.notice-error' ).length ) { $modal.find( '.notice-warning' ).remove(); } } this.$overlay.html( this.overlay.el ); // Bind to theme:next and theme:previous triggered by the arrow keys. // Keep track of the current model so we can infer an index position. this.listenTo( this.overlay, 'theme:next', function() { // Renders the next theme on the overlay. self.next( [ self.model.cid ] ); }) .listenTo( this.overlay, 'theme:previous', function() { // Renders the previous theme on the overlay. self.previous( [ self.model.cid ] ); }); }, /* * This method renders the next theme on the overlay modal * based on the current position in the collection. * * @params [model cid] */ next: function( args ) { var self = this, model, nextModel; // Get the current theme. model = self.collection.get( args[0] ); // Find the next model within the collection. nextModel = self.collection.at( self.collection.indexOf( model ) + 1 ); // Confidence check which also serves as a boundary test. if ( nextModel !== undefined ) { // We have a new theme... // Close the overlay. this.overlay.closeOverlay(); // Trigger a route update for the current model. self.theme.trigger( 'theme:expand', nextModel.cid ); } }, /* * This method renders the previous theme on the overlay modal * based on the current position in the collection. * * @params [model cid] */ previous: function( args ) { var self = this, model, previousModel; // Get the current theme. model = self.collection.get( args[0] ); // Find the previous model within the collection. previousModel = self.collection.at( self.collection.indexOf( model ) - 1 ); if ( previousModel !== undefined ) { // We have a new theme... // Close the overlay. this.overlay.closeOverlay(); // Trigger a route update for the current model. self.theme.trigger( 'theme:expand', previousModel.cid ); } }, // Dispatch audible search results feedback message. announceSearchResults: function( count ) { if ( 0 === count ) { wp.a11y.speak( l10n.noThemesFound ); } else { wp.a11y.speak( l10n.themesFound.replace( '%d', count ) ); } } }); // Search input view controller. themes.view.Search = wp.Backbone.View.extend({ tagName: 'input', className: 'wp-filter-search', id: 'wp-filter-search-input', searching: false, attributes: { type: 'search', 'aria-describedby': 'live-search-desc' }, events: { 'input': 'search', 'keyup': 'search', 'blur': 'pushState' }, initialize: function( options ) { this.parent = options.parent; this.listenTo( this.parent, 'theme:close', function() { this.searching = false; } ); }, search: function( event ) { // Clear on escape. if ( event.type === 'keyup' && event.which === 27 ) { event.target.value = ''; } // Since doSearch is debounced, it will only run when user input comes to a rest. this.doSearch( event ); }, // Runs a search on the theme collection. doSearch: function( event ) { var options = {}; this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) ); // if search is initiated and key is not return. if ( this.searching && event.which !== 13 ) { options.replace = true; } else { this.searching = true; } // Update the URL hash. if ( event.target.value ) { themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options ); } else { themes.router.navigate( themes.router.baseUrl( '' ) ); } }, pushState: function( event ) { var url = themes.router.baseUrl( '' ); if ( event.target.value ) { url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) ); } this.searching = false; themes.router.navigate( url ); } }); /** * Navigate router. * * @since 4.9.0 * * @param {string} url - URL to navigate to. * @param {Object} state - State. * @return {void} */ function navigateRouter( url, state ) { var router = this; if ( Backbone.history._hasPushState ) { Backbone.Router.prototype.navigate.call( router, url, state ); } } // Sets up the routes events for relevant url queries. // Listens to [theme] and [search] params. themes.Router = Backbone.Router.extend({ routes: { 'themes.php?theme=:slug': 'theme', 'themes.php?search=:query': 'search', 'themes.php?s=:query': 'search', 'themes.php': 'themes', '': 'themes' }, baseUrl: function( url ) { return 'themes.php' + url; }, themePath: '?theme=', searchPath: '?search=', search: function( query ) { $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) ); }, themes: function() { $( '.wp-filter-search' ).val( '' ); }, navigate: navigateRouter }); // Execute and setup the application. themes.Run = { init: function() { // Initializes the blog's theme library view. // Create a new collection with data. this.themes = new themes.Collection( themes.data.themes ); // Set up the view. this.view = new themes.view.Appearance({ collection: this.themes }); this.render(); // Start debouncing user searches after Backbone.history.start(). this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 ); }, render: function() { // Render results. this.view.render(); this.routes(); if ( Backbone.History.started ) { Backbone.history.stop(); } Backbone.history.start({ root: themes.data.settings.adminUrl, pushState: true, hashChange: false }); }, routes: function() { var self = this; // Bind to our global thx object // so that the object is available to sub-views. themes.router = new themes.Router(); // Handles theme details route event. themes.router.on( 'route:theme', function( slug ) { self.view.view.expand( slug ); }); themes.router.on( 'route:themes', function() { self.themes.doSearch( '' ); self.view.trigger( 'theme:close' ); }); // Handles search route event. themes.router.on( 'route:search', function() { $( '.wp-filter-search' ).trigger( 'keyup' ); }); this.extraRoutes(); }, extraRoutes: function() { return false; } }; // Extend the main Search view. themes.view.InstallerSearch = themes.view.Search.extend({ events: { 'input': 'search', 'keyup': 'search' }, terms: '', // Handles Ajax request for searching through themes in public repo. search: function( event ) { // Tabbing or reverse tabbing into the search input shouldn't trigger a search. if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) { return; } this.collection = this.options.parent.view.collection; // Clear on escape. if ( event.type === 'keyup' && event.which === 27 ) { event.target.value = ''; } this.doSearch( event.target.value ); }, doSearch: function( value ) { var request = {}; // Don't do anything if the search terms haven't changed. if ( this.terms === value ) { return; } // Updates terms with the value passed. this.terms = value; request.search = value; /* * Intercept an [author] search. * * If input value starts with `author:` send a request * for `author` instead of a regular `search`. */ if ( value.substring( 0, 7 ) === 'author:' ) { request.search = ''; request.author = value.slice( 7 ); } /* * Intercept a [tag] search. * * If input value starts with `tag:` send a request * for `tag` instead of a regular `search`. */ if ( value.substring( 0, 4 ) === 'tag:' ) { request.search = ''; request.tag = [ value.slice( 4 ) ]; } $( '.filter-links li > a.current' ) .removeClass( 'current' ) .removeAttr( 'aria-current' ); $( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' ); $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' ); // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( request ); // Set route. themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } ); } }); themes.view.Installer = themes.view.Appearance.extend({ el: '#wpbody-content .wrap', // Register events for sorting and filters in theme-navigation. events: { 'click .filter-links li > a': 'onSort', 'click .theme-filter': 'onFilter', 'click .drawer-toggle': 'moreFilters', 'click .filter-drawer .apply-filters': 'applyFilters', 'click .filter-group [type="checkbox"]': 'addFilter', 'click .filter-drawer .clear-filters': 'clearFilters', 'click .edit-filters': 'backToFilters', 'click .favorites-form-submit' : 'saveUsername', 'keyup #wporg-username-input': 'saveUsername' }, // Initial render method. render: function() { var self = this; this.search(); this.uploader(); this.collection = new themes.Collection(); // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page. this.listenTo( this, 'theme:end', function() { // Make sure we are not already loading. if ( self.collection.loadingThemes ) { return; } // Set loadingThemes to true and bump page instance of currentQuery. self.collection.loadingThemes = true; self.collection.currentQuery.page++; // Use currentQuery.page to build the themes request. _.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } ); self.collection.query( self.collection.currentQuery.request ); }); this.listenTo( this.collection, 'query:success', function() { $( 'body' ).removeClass( 'loading-content' ); $( '.theme-browser' ).find( 'div.error' ).remove(); }); this.listenTo( this.collection, 'query:fail', function() { $( 'body' ).removeClass( 'loading-content' ); $( '.theme-browser' ).find( 'div.error' ).remove(); $( '.theme-browser' ).find( 'div.themes' ).before( '

    ' + l10n.error + '

    ' ); $( '.theme-browser .error .try-again' ).on( 'click', function( e ) { e.preventDefault(); $( 'input.wp-filter-search' ).trigger( 'input' ); } ); }); if ( this.view ) { this.view.remove(); } // Sets up the view and passes the section argument. this.view = new themes.view.Themes({ collection: this.collection, parent: this }); // Reset pagination every time the install view handler is run. this.page = 0; // Render and append. this.$el.find( '.themes' ).remove(); this.view.render(); this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' ); }, // Handles all the rendering of the public theme directory. browse: function( section ) { // Create a new collection with the proper theme data // for each section. if ( 'block-themes' === section ) { // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( { tag: 'full-site-editing' } ); } else { this.collection.query( { browse: section } ); } }, // Sorting navigation. onSort: function( event ) { var $el = $( event.target ), sort = $el.data( 'sort' ); event.preventDefault(); $( 'body' ).removeClass( 'filters-applied show-filters' ); $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' ); // Bail if this is already active. if ( $el.hasClass( this.activeClass ) ) { return; } this.sort( sort ); // Trigger a router.navigate update. themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) ); }, sort: function( sort ) { this.clearSearch(); // Track sorting so we can restore the correct tab when closing preview. themes.router.selectedTab = sort; $( '.filter-links li > a, .theme-filter' ) .removeClass( this.activeClass ) .removeAttr( 'aria-current' ); $( '[data-sort="' + sort + '"]' ) .addClass( this.activeClass ) .attr( 'aria-current', 'page' ); if ( 'favorites' === sort ) { $( 'body' ).addClass( 'show-favorites-form' ); } else { $( 'body' ).removeClass( 'show-favorites-form' ); } this.browse( sort ); }, // Filters and Tags. onFilter: function( event ) { var request, $el = $( event.target ), filter = $el.data( 'filter' ); // Bail if this is already active. if ( $el.hasClass( this.activeClass ) ) { return; } $( '.filter-links li > a, .theme-section' ) .removeClass( this.activeClass ) .removeAttr( 'aria-current' ); $el .addClass( this.activeClass ) .attr( 'aria-current', 'page' ); if ( ! filter ) { return; } // Construct the filter request // using the default values. filter = _.union( [ filter, this.filtersChecked() ] ); request = { tag: [ filter ] }; // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( request ); }, // Clicking on a checkbox to add another filter to the request. addFilter: function() { this.filtersChecked(); }, // Applying filters triggers a tag request. applyFilters: function( event ) { var name, tags = this.filtersChecked(), request = { tag: tags }, filteringBy = $( '.filtered-by .tags' ); if ( event ) { event.preventDefault(); } if ( ! tags ) { wp.a11y.speak( l10n.selectFeatureFilter ); return; } $( 'body' ).addClass( 'filters-applied' ); $( '.filter-links li > a.current' ) .removeClass( 'current' ) .removeAttr( 'aria-current' ); filteringBy.empty(); _.each( tags, function( tag ) { name = $( 'label[for="filter-id-' + tag + '"]' ).text(); filteringBy.append( '' + name + '' ); }); // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( request ); }, // Save the user's WordPress.org username and get his favorite themes. saveUsername: function ( event ) { var username = $( '#wporg-username-input' ).val(), nonce = $( '#wporg-username-nonce' ).val(), request = { browse: 'favorites', user: username }, that = this; if ( event ) { event.preventDefault(); } // Save username on enter. if ( event.type === 'keyup' && event.which !== 13 ) { return; } return wp.ajax.send( 'save-wporg-username', { data: { _wpnonce: nonce, username: username }, success: function () { // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. that.collection.query( request ); } } ); }, /** * Get the checked filters. * * @return {Array} of tags or false */ filtersChecked: function() { var items = $( '.filter-group' ).find( ':checkbox' ), tags = []; _.each( items.filter( ':checked' ), function( item ) { tags.push( $( item ).prop( 'value' ) ); }); // When no filters are checked, restore initial state and return. if ( tags.length === 0 ) { $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' ); $( '.filter-drawer .clear-filters' ).hide(); $( 'body' ).removeClass( 'filters-applied' ); return false; } $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length ); $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' ); return tags; }, activeClass: 'current', /** * When users press the "Upload Theme" button, show the upload form in place. */ uploader: function() { var uploadViewToggle = $( '.upload-view-toggle' ), $body = $( document.body ); uploadViewToggle.on( 'click', function() { // Toggle the upload view. $body.toggleClass( 'show-upload-view' ); // Toggle the `aria-expanded` button attribute. uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) ); }); }, // Toggle the full filters navigation. moreFilters: function( event ) { var $body = $( 'body' ), $toggleButton = $( '.drawer-toggle' ); event.preventDefault(); if ( $body.hasClass( 'filters-applied' ) ) { return this.backToFilters(); } this.clearSearch(); themes.router.navigate( themes.router.baseUrl( '' ) ); // Toggle the feature filters view. $body.toggleClass( 'show-filters' ); // Toggle the `aria-expanded` button attribute. $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) ); }, /** * Clears all the checked filters. * * @uses filtersChecked() */ clearFilters: function( event ) { var items = $( '.filter-group' ).find( ':checkbox' ), self = this; event.preventDefault(); _.each( items.filter( ':checked' ), function( item ) { $( item ).prop( 'checked', false ); return self.filtersChecked(); }); }, backToFilters: function( event ) { if ( event ) { event.preventDefault(); } $( 'body' ).removeClass( 'filters-applied' ); }, clearSearch: function() { $( '#wp-filter-search-input').val( '' ); } }); themes.InstallerRouter = Backbone.Router.extend({ routes: { 'theme-install.php?theme=:slug': 'preview', 'theme-install.php?browse=:sort': 'sort', 'theme-install.php?search=:query': 'search', 'theme-install.php': 'sort' }, baseUrl: function( url ) { return 'theme-install.php' + url; }, themePath: '?theme=', browsePath: '?browse=', searchPath: '?search=', search: function( query ) { $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) ); }, navigate: navigateRouter }); themes.RunInstaller = { init: function() { // Set up the view. // Passes the default 'section' as an option. this.view = new themes.view.Installer({ section: 'popular', SearchView: themes.view.InstallerSearch }); // Render results. this.render(); // Start debouncing user searches after Backbone.history.start(). this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 ); }, render: function() { // Render results. this.view.render(); this.routes(); if ( Backbone.History.started ) { Backbone.history.stop(); } Backbone.history.start({ root: themes.data.settings.adminUrl, pushState: true, hashChange: false }); }, routes: function() { var self = this, request = {}; // Bind to our global `wp.themes` object // so that the router is available to sub-views. themes.router = new themes.InstallerRouter(); // Handles `theme` route event. // Queries the API for the passed theme slug. themes.router.on( 'route:preview', function( slug ) { // Remove existing handlers. if ( themes.preview ) { themes.preview.undelegateEvents(); themes.preview.unbind(); } // If the theme preview is active, set the current theme. if ( self.view.view.theme && self.view.view.theme.preview ) { self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } ); self.view.view.theme.preview(); } else { // Select the theme by slug. request.theme = slug; self.view.collection.query( request ); self.view.collection.trigger( 'update' ); // Open the theme preview. self.view.collection.once( 'query:success', function() { $( 'div[data-slug="' + slug + '"]' ).trigger( 'click' ); }); } }); /* * Handles sorting / browsing routes. * Also handles the root URL triggering a sort request * for `popular`, the default view. */ themes.router.on( 'route:sort', function( sort ) { if ( ! sort ) { sort = 'popular'; themes.router.navigate( themes.router.baseUrl( '?browse=popular' ), { replace: true } ); } self.view.sort( sort ); // Close the preview if open. if ( themes.preview ) { themes.preview.close(); } }); // The `search` route event. The router populates the input field. themes.router.on( 'route:search', function() { $( '.wp-filter-search' ).trigger( 'focus' ).trigger( 'keyup' ); }); this.extraRoutes(); }, extraRoutes: function() { return false; } }; // Ready... $( function() { if ( themes.isInstall ) { themes.RunInstaller.init(); } else { themes.Run.init(); } // Update the return param just in time. $( document.body ).on( 'click', '.load-customize', function() { var link = $( this ), urlParser = document.createElement( 'a' ); urlParser.href = link.prop( 'href' ); urlParser.search = $.param( _.extend( wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ), { 'return': window.location.href } ) ); link.prop( 'href', urlParser.href ); }); $( '.broken-themes .delete-theme' ).on( 'click', function() { return confirm( _wpThemeSettings.settings.confirmDelete ); }); }); })( jQuery ); // Align theme browser thickbox. jQuery( function($) { window.tb_position = function() { var tbWindow = $('#TB_window'), width = $(window).width(), H = $(window).height(), W = ( 1040 < width ) ? 1040 : width, adminbar_height = 0; if ( $('#wpadminbar').length ) { adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 ); } if ( tbWindow.length >= 1 ) { tbWindow.width( W - 50 ).height( H - 45 - adminbar_height ); $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height ); tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'}); if ( typeof document.body.style.maxWidth !== 'undefined' ) { tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'}); } } }; $(window).on( 'resize', function(){ tb_position(); }); }); PK1YZ "//customize-widgets.jsnu[/** * @output wp-admin/js/customize-widgets.js */ /* global _wpCustomizeWidgetsSettings */ (function( wp, $ ){ if ( ! wp || ! wp.customize ) { return; } // Set up our namespace... var api = wp.customize, l10n; /** * @namespace wp.customize.Widgets */ api.Widgets = api.Widgets || {}; api.Widgets.savedWidgetIds = {}; // Link settings. api.Widgets.data = _wpCustomizeWidgetsSettings || {}; l10n = api.Widgets.data.l10n; /** * wp.customize.Widgets.WidgetModel * * A single widget model. * * @class wp.customize.Widgets.WidgetModel * @augments Backbone.Model */ api.Widgets.WidgetModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.WidgetModel.prototype */{ id: null, temp_id: null, classname: null, control_tpl: null, description: null, is_disabled: null, is_multi: null, multi_number: null, name: null, id_base: null, transport: null, params: [], width: null, height: null, search_matched: true }); /** * wp.customize.Widgets.WidgetCollection * * Collection for widget models. * * @class wp.customize.Widgets.WidgetCollection * @augments Backbone.Collection */ api.Widgets.WidgetCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.WidgetCollection.prototype */{ model: api.Widgets.WidgetModel, // Controls searching on the current widget collection // and triggers an update event. doSearch: function( value ) { // Don't do anything if we've already done this search. // Useful because the search handler fires multiple times per keystroke. if ( this.terms === value ) { return; } // Updates terms with the value passed. this.terms = value; // If we have terms, run a search... if ( this.terms.length > 0 ) { this.search( this.terms ); } // If search is blank, set all the widgets as they matched the search to reset the views. if ( this.terms === '' ) { this.each( function ( widget ) { widget.set( 'search_matched', true ); } ); } }, // Performs a search within the collection. // @uses RegExp search: function( term ) { var match, haystack; // Escape the term string for RegExp meta characters. term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ); // Consider spaces as word delimiters and match the whole string // so matching terms can be combined. term = term.replace( / /g, ')(?=.*' ); match = new RegExp( '^(?=.*' + term + ').+', 'i' ); this.each( function ( data ) { haystack = [ data.get( 'name' ), data.get( 'description' ) ].join( ' ' ); data.set( 'search_matched', match.test( haystack ) ); } ); } }); api.Widgets.availableWidgets = new api.Widgets.WidgetCollection( api.Widgets.data.availableWidgets ); /** * wp.customize.Widgets.SidebarModel * * A single sidebar model. * * @class wp.customize.Widgets.SidebarModel * @augments Backbone.Model */ api.Widgets.SidebarModel = Backbone.Model.extend(/** @lends wp.customize.Widgets.SidebarModel.prototype */{ after_title: null, after_widget: null, before_title: null, before_widget: null, 'class': null, description: null, id: null, name: null, is_rendered: false }); /** * wp.customize.Widgets.SidebarCollection * * Collection for sidebar models. * * @class wp.customize.Widgets.SidebarCollection * @augments Backbone.Collection */ api.Widgets.SidebarCollection = Backbone.Collection.extend(/** @lends wp.customize.Widgets.SidebarCollection.prototype */{ model: api.Widgets.SidebarModel }); api.Widgets.registeredSidebars = new api.Widgets.SidebarCollection( api.Widgets.data.registeredSidebars ); api.Widgets.AvailableWidgetsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Widgets.AvailableWidgetsPanelView.prototype */{ el: '#available-widgets', events: { 'input #widgets-search': 'search', 'focus .widget-tpl' : 'focus', 'click .widget-tpl' : '_submit', 'keypress .widget-tpl' : '_submit', 'keydown' : 'keyboardAccessible' }, // Cache current selected widget. selected: null, // Cache sidebar control which has opened panel. currentSidebarControl: null, $search: null, $clearResults: null, searchMatchesCount: null, /** * View class for the available widgets panel. * * @constructs wp.customize.Widgets.AvailableWidgetsPanelView * @augments wp.Backbone.View */ initialize: function() { var self = this; this.$search = $( '#widgets-search' ); this.$clearResults = this.$el.find( '.clear-results' ); _.bindAll( this, 'close' ); this.listenTo( this.collection, 'change', this.updateList ); this.updateList(); // Set the initial search count to the number of available widgets. this.searchMatchesCount = this.collection.length; /* * If the available widgets panel is open and the customize controls * are interacted with (i.e. available widgets panel is blurred) then * close the available widgets panel. Also close on back button click. */ $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) { var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' ); if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) { self.close(); } } ); // Clear the search results and trigger an `input` event to fire a new search. this.$clearResults.on( 'click', function() { self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' ); } ); // Close the panel if the URL in the preview changes. api.previewer.bind( 'url', this.close ); }, /** * Performs a search and handles selected widget. */ search: _.debounce( function( event ) { var firstVisible; this.collection.doSearch( event.target.value ); // Update the search matches count. this.updateSearchMatchesCount(); // Announce how many search results. this.announceSearchMatches(); // Remove a widget from being selected if it is no longer visible. if ( this.selected && ! this.selected.is( ':visible' ) ) { this.selected.removeClass( 'selected' ); this.selected = null; } // If a widget was selected but the filter value has been cleared out, clear selection. if ( this.selected && ! event.target.value ) { this.selected.removeClass( 'selected' ); this.selected = null; } // If a filter has been entered and a widget hasn't been selected, select the first one shown. if ( ! this.selected && event.target.value ) { firstVisible = this.$el.find( '> .widget-tpl:visible:first' ); if ( firstVisible.length ) { this.select( firstVisible ); } } // Toggle the clear search results button. if ( '' !== event.target.value ) { this.$clearResults.addClass( 'is-visible' ); } else if ( '' === event.target.value ) { this.$clearResults.removeClass( 'is-visible' ); } // Set a CSS class on the search container when there are no search results. if ( ! this.searchMatchesCount ) { this.$el.addClass( 'no-widgets-found' ); } else { this.$el.removeClass( 'no-widgets-found' ); } }, 500 ), /** * Updates the count of the available widgets that have the `search_matched` attribute. */ updateSearchMatchesCount: function() { this.searchMatchesCount = this.collection.where({ search_matched: true }).length; }, /** * Sends a message to the aria-live region to announce how many search results. */ announceSearchMatches: function() { var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ; if ( ! this.searchMatchesCount ) { message = l10n.noWidgetsFound; } wp.a11y.speak( message ); }, /** * Changes visibility of available widgets. */ updateList: function() { this.collection.each( function( widget ) { var widgetTpl = $( '#widget-tpl-' + widget.id ); widgetTpl.toggle( widget.get( 'search_matched' ) && ! widget.get( 'is_disabled' ) ); if ( widget.get( 'is_disabled' ) && widgetTpl.is( this.selected ) ) { this.selected = null; } } ); }, /** * Highlights a widget. */ select: function( widgetTpl ) { this.selected = $( widgetTpl ); this.selected.siblings( '.widget-tpl' ).removeClass( 'selected' ); this.selected.addClass( 'selected' ); }, /** * Highlights a widget on focus. */ focus: function( event ) { this.select( $( event.currentTarget ) ); }, /** * Handles submit for keypress and click on widget. */ _submit: function( event ) { // Only proceed with keypress if it is Enter or Spacebar. if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { return; } this.submit( $( event.currentTarget ) ); }, /** * Adds a selected widget to the sidebar. */ submit: function( widgetTpl ) { var widgetId, widget, widgetFormControl; if ( ! widgetTpl ) { widgetTpl = this.selected; } if ( ! widgetTpl || ! this.currentSidebarControl ) { return; } this.select( widgetTpl ); widgetId = $( this.selected ).data( 'widget-id' ); widget = this.collection.findWhere( { id: widgetId } ); if ( ! widget ) { return; } widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) ); if ( widgetFormControl ) { widgetFormControl.focus(); } this.close(); }, /** * Opens the panel. */ open: function( sidebarControl ) { this.currentSidebarControl = sidebarControl; // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens. _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) { if ( control.params.is_wide ) { control.collapseForm(); } } ); if ( api.section.has( 'publish_settings' ) ) { api.section( 'publish_settings' ).collapse(); } $( 'body' ).addClass( 'adding-widget' ); this.$el.find( '.selected' ).removeClass( 'selected' ); // Reset search. this.collection.doSearch( '' ); if ( ! api.settings.browser.mobile ) { this.$search.trigger( 'focus' ); } }, /** * Closes the panel. */ close: function( options ) { options = options || {}; if ( options.returnFocus && this.currentSidebarControl ) { this.currentSidebarControl.container.find( '.add-new-widget' ).focus(); } this.currentSidebarControl = null; this.selected = null; $( 'body' ).removeClass( 'adding-widget' ); this.$search.val( '' ).trigger( 'input' ); }, /** * Adds keyboard accessibility to the panel. */ keyboardAccessible: function( event ) { var isEnter = ( event.which === 13 ), isEsc = ( event.which === 27 ), isDown = ( event.which === 40 ), isUp = ( event.which === 38 ), isTab = ( event.which === 9 ), isShift = ( event.shiftKey ), selected = null, firstVisible = this.$el.find( '> .widget-tpl:visible:first' ), lastVisible = this.$el.find( '> .widget-tpl:visible:last' ), isSearchFocused = $( event.target ).is( this.$search ), isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' ); if ( isDown || isUp ) { if ( isDown ) { if ( isSearchFocused ) { selected = firstVisible; } else if ( this.selected && this.selected.nextAll( '.widget-tpl:visible' ).length !== 0 ) { selected = this.selected.nextAll( '.widget-tpl:visible:first' ); } } else if ( isUp ) { if ( isSearchFocused ) { selected = lastVisible; } else if ( this.selected && this.selected.prevAll( '.widget-tpl:visible' ).length !== 0 ) { selected = this.selected.prevAll( '.widget-tpl:visible:first' ); } } this.select( selected ); if ( selected ) { selected.trigger( 'focus' ); } else { this.$search.trigger( 'focus' ); } return; } // If enter pressed but nothing entered, don't do anything. if ( isEnter && ! this.$search.val() ) { return; } if ( isEnter ) { this.submit(); } else if ( isEsc ) { this.close( { returnFocus: true } ); } if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) { this.currentSidebarControl.container.find( '.add-new-widget' ).focus(); event.preventDefault(); } } }); /** * Handlers for the widget-synced event, organized by widget ID base. * Other widgets may provide their own update handlers by adding * listeners for the widget-synced event. * * @alias wp.customize.Widgets.formSyncHandlers */ api.Widgets.formSyncHandlers = { /** * @param {jQuery.Event} e * @param {jQuery} widget * @param {string} newForm */ rss: function( e, widget, newForm ) { var oldWidgetError = widget.find( '.widget-error:first' ), newWidgetError = $( '
    ' + newForm + '
    ' ).find( '.widget-error:first' ); if ( oldWidgetError.length && newWidgetError.length ) { oldWidgetError.replaceWith( newWidgetError ); } else if ( oldWidgetError.length ) { oldWidgetError.remove(); } else if ( newWidgetError.length ) { widget.find( '.widget-content:first' ).prepend( newWidgetError ); } } }; api.Widgets.WidgetControl = api.Control.extend(/** @lends wp.customize.Widgets.WidgetControl.prototype */{ defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop }, /** * wp.customize.Widgets.WidgetControl * * Customizer control for widgets. * Note that 'widget_form' must match the WP_Widget_Form_Customize_Control::$type * * @since 4.1.0 * * @constructs wp.customize.Widgets.WidgetControl * @augments wp.customize.Control */ initialize: function( id, options ) { var control = this; control.widgetControlEmbedded = false; control.widgetContentEmbedded = false; control.expanded = new api.Value( false ); control.expandedArgumentsQueue = []; control.expanded.bind( function( expanded ) { var args = control.expandedArgumentsQueue.shift(); args = $.extend( {}, control.defaultExpandedArguments, args ); control.onChangeExpanded( expanded, args ); }); control.altNotice = true; api.Control.prototype.initialize.call( control, id, options ); }, /** * Set up the control. * * @since 3.9.0 */ ready: function() { var control = this; /* * Embed a placeholder once the section is expanded. The full widget * form content will be embedded once the control itself is expanded, * and at this point the widget-added event will be triggered. */ if ( ! control.section() ) { control.embedWidgetControl(); } else { api.section( control.section(), function( section ) { var onExpanded = function( isExpanded ) { if ( isExpanded ) { control.embedWidgetControl(); section.expanded.unbind( onExpanded ); } }; if ( section.expanded() ) { onExpanded( true ); } else { section.expanded.bind( onExpanded ); } } ); } }, /** * Embed the .widget element inside the li container. * * @since 4.4.0 */ embedWidgetControl: function() { var control = this, widgetControl; if ( control.widgetControlEmbedded ) { return; } control.widgetControlEmbedded = true; widgetControl = $( control.params.widget_control ); control.container.append( widgetControl ); control._setupModel(); control._setupWideWidget(); control._setupControlToggle(); control._setupWidgetTitle(); control._setupReorderUI(); control._setupHighlightEffects(); control._setupUpdateUI(); control._setupRemoveUI(); }, /** * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event. * * @since 4.4.0 */ embedWidgetContent: function() { var control = this, widgetContent; control.embedWidgetControl(); if ( control.widgetContentEmbedded ) { return; } control.widgetContentEmbedded = true; // Update the notification container element now that the widget content has been embedded. control.notifications.container = control.getNotificationsContainerElement(); control.notifications.render(); widgetContent = $( control.params.widget_content ); control.container.find( '.widget-content:first' ).append( widgetContent ); /* * Trigger widget-added event so that plugins can attach any event * listeners and dynamic UI elements. */ $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] ); }, /** * Handle changes to the setting */ _setupModel: function() { var self = this, rememberSavedWidgetId; // Remember saved widgets so we know which to trash (move to inactive widgets sidebar). rememberSavedWidgetId = function() { api.Widgets.savedWidgetIds[self.params.widget_id] = true; }; api.bind( 'ready', rememberSavedWidgetId ); api.bind( 'saved', rememberSavedWidgetId ); this._updateCount = 0; this.isWidgetUpdating = false; this.liveUpdateMode = true; // Update widget whenever model changes. this.setting.bind( function( to, from ) { if ( ! _( from ).isEqual( to ) && ! self.isWidgetUpdating ) { self.updateWidget( { instance: to } ); } } ); }, /** * Add special behaviors for wide widget controls */ _setupWideWidget: function() { var self = this, $widgetInside, $widgetForm, $customizeSidebar, $themeControlsContainer, positionWidget; if ( ! this.params.is_wide || $( window ).width() <= 640 /* max-width breakpoint in customize-controls.css */ ) { return; } $widgetInside = this.container.find( '.widget-inside' ); $widgetForm = $widgetInside.find( '> .form' ); $customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' ); this.container.addClass( 'wide-widget-control' ); this.container.find( '.form:first' ).css( { 'max-width': this.params.width, 'min-height': this.params.height } ); /** * Keep the widget-inside positioned so the top of fixed-positioned * element is at the same top position as the widget-top. When the * widget-top is scrolled out of view, keep the widget-top in view; * likewise, don't allow the widget to drop off the bottom of the window. * If a widget is too tall to fit in the window, don't let the height * exceed the window height so that the contents of the widget control * will become scrollable (overflow:auto). */ positionWidget = function() { var offsetTop = self.container.offset().top, windowHeight = $( window ).height(), formHeight = $widgetForm.outerHeight(), top; $widgetInside.css( 'max-height', windowHeight ); top = Math.max( 0, // Prevent top from going off screen. Math.min( Math.max( offsetTop, 0 ), // Distance widget in panel is from top of screen. windowHeight - formHeight // Flush up against bottom of screen. ) ); $widgetInside.css( 'top', top ); }; $themeControlsContainer = $( '#customize-theme-controls' ); this.container.on( 'expand', function() { positionWidget(); $customizeSidebar.on( 'scroll', positionWidget ); $( window ).on( 'resize', positionWidget ); $themeControlsContainer.on( 'expanded collapsed', positionWidget ); } ); this.container.on( 'collapsed', function() { $customizeSidebar.off( 'scroll', positionWidget ); $( window ).off( 'resize', positionWidget ); $themeControlsContainer.off( 'expanded collapsed', positionWidget ); } ); // Reposition whenever a sidebar's widgets are changed. api.each( function( setting ) { if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) { setting.bind( function() { if ( self.container.hasClass( 'expanded' ) ) { positionWidget(); } } ); } } ); }, /** * Show/hide the control when clicking on the form title, when clicking * the close button */ _setupControlToggle: function() { var self = this, $closeBtn; this.container.find( '.widget-top' ).on( 'click', function( e ) { e.preventDefault(); var sidebarWidgetsControl = self.getSidebarWidgetsControl(); if ( sidebarWidgetsControl.isReordering ) { return; } self.expanded( ! self.expanded() ); } ); $closeBtn = this.container.find( '.widget-control-close' ); $closeBtn.on( 'click', function() { self.collapse(); self.container.find( '.widget-top .widget-action:first' ).focus(); // Keyboard accessibility. } ); }, /** * Update the title of the form if a title field is entered */ _setupWidgetTitle: function() { var self = this, updateTitle; updateTitle = function() { var title = self.setting().title, inWidgetTitle = self.container.find( '.in-widget-title' ); if ( title ) { inWidgetTitle.text( ': ' + title ); } else { inWidgetTitle.text( '' ); } }; this.setting.bind( updateTitle ); updateTitle(); }, /** * Set up the widget-reorder-nav */ _setupReorderUI: function() { var self = this, selectSidebarItem, $moveWidgetArea, $reorderNav, updateAvailableSidebars, template; /** * select the provided sidebar list item in the move widget area * * @param {jQuery} li */ selectSidebarItem = function( li ) { li.siblings( '.selected' ).removeClass( 'selected' ); li.addClass( 'selected' ); var isSelfSidebar = ( li.data( 'id' ) === self.params.sidebar_id ); self.container.find( '.move-widget-btn' ).prop( 'disabled', isSelfSidebar ); }; /** * Add the widget reordering elements to the widget control */ this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) ); template = _.template( api.Widgets.data.tpl.moveWidgetArea ); $moveWidgetArea = $( template( { sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' ) } ) ); this.container.find( '.widget-top' ).after( $moveWidgetArea ); /** * Update available sidebars when their rendered state changes */ updateAvailableSidebars = function() { var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem, renderedSidebarCount = 0; selfSidebarItem = $sidebarItems.filter( function(){ return $( this ).data( 'id' ) === self.params.sidebar_id; } ); $sidebarItems.each( function() { var li = $( this ), sidebarId, sidebar, sidebarIsRendered; sidebarId = li.data( 'id' ); sidebar = api.Widgets.registeredSidebars.get( sidebarId ); sidebarIsRendered = sidebar.get( 'is_rendered' ); li.toggle( sidebarIsRendered ); if ( sidebarIsRendered ) { renderedSidebarCount += 1; } if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) { selectSidebarItem( selfSidebarItem ); } } ); if ( renderedSidebarCount > 1 ) { self.container.find( '.move-widget' ).show(); } else { self.container.find( '.move-widget' ).hide(); } }; updateAvailableSidebars(); api.Widgets.registeredSidebars.on( 'change:is_rendered', updateAvailableSidebars ); /** * Handle clicks for up/down/move on the reorder nav */ $reorderNav = this.container.find( '.widget-reorder-nav' ); $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() { $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' ); } ).on( 'click keypress', function( event ) { if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { return; } $( this ).trigger( 'focus' ); if ( $( this ).is( '.move-widget' ) ) { self.toggleWidgetMoveArea(); } else { var isMoveDown = $( this ).is( '.move-widget-down' ), isMoveUp = $( this ).is( '.move-widget-up' ), i = self.getWidgetSidebarPosition(); if ( ( isMoveUp && i === 0 ) || ( isMoveDown && i === self.getSidebarWidgetsControl().setting().length - 1 ) ) { return; } if ( isMoveUp ) { self.moveUp(); wp.a11y.speak( l10n.widgetMovedUp ); } else { self.moveDown(); wp.a11y.speak( l10n.widgetMovedDown ); } $( this ).trigger( 'focus' ); // Re-focus after the container was moved. } } ); /** * Handle selecting a sidebar to move to */ this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) { if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { return; } event.preventDefault(); selectSidebarItem( $( this ) ); } ); /** * Move widget to another sidebar */ this.container.find( '.move-widget-btn' ).click( function() { self.getSidebarWidgetsControl().toggleReordering( false ); var oldSidebarId = self.params.sidebar_id, newSidebarId = self.container.find( '.widget-area-select li.selected' ).data( 'id' ), oldSidebarWidgetsSetting, newSidebarWidgetsSetting, oldSidebarWidgetIds, newSidebarWidgetIds, i; oldSidebarWidgetsSetting = api( 'sidebars_widgets[' + oldSidebarId + ']' ); newSidebarWidgetsSetting = api( 'sidebars_widgets[' + newSidebarId + ']' ); oldSidebarWidgetIds = Array.prototype.slice.call( oldSidebarWidgetsSetting() ); newSidebarWidgetIds = Array.prototype.slice.call( newSidebarWidgetsSetting() ); i = self.getWidgetSidebarPosition(); oldSidebarWidgetIds.splice( i, 1 ); newSidebarWidgetIds.push( self.params.widget_id ); oldSidebarWidgetsSetting( oldSidebarWidgetIds ); newSidebarWidgetsSetting( newSidebarWidgetIds ); self.focus(); } ); }, /** * Highlight widgets in preview when interacted with in the Customizer */ _setupHighlightEffects: function() { var self = this; // Highlight whenever hovering or clicking over the form. this.container.on( 'mouseenter click', function() { self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); } ); // Highlight when the setting is updated. this.setting.bind( function() { self.setting.previewer.send( 'highlight-widget', self.params.widget_id ); } ); }, /** * Set up event handlers for widget updating */ _setupUpdateUI: function() { var self = this, $widgetRoot, $widgetContent, $saveBtn, updateWidgetDebounced, formSyncHandler; $widgetRoot = this.container.find( '.widget:first' ); $widgetContent = $widgetRoot.find( '.widget-content:first' ); // Configure update button. $saveBtn = this.container.find( '.widget-control-save' ); $saveBtn.val( l10n.saveBtnLabel ); $saveBtn.attr( 'title', l10n.saveBtnTooltip ); $saveBtn.removeClass( 'button-primary' ); $saveBtn.on( 'click', function( e ) { e.preventDefault(); self.updateWidget( { disable_form: true } ); // @todo disable_form is unused? } ); updateWidgetDebounced = _.debounce( function() { self.updateWidget(); }, 250 ); // Trigger widget form update when hitting Enter within an input. $widgetContent.on( 'keydown', 'input', function( e ) { if ( 13 === e.which ) { // Enter. e.preventDefault(); self.updateWidget( { ignoreActiveElement: true } ); } } ); // Handle widgets that support live previews. $widgetContent.on( 'change input propertychange', ':input', function( e ) { if ( ! self.liveUpdateMode ) { return; } if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) { updateWidgetDebounced(); } } ); // Remove loading indicators when the setting is saved and the preview updates. this.setting.previewer.channel.bind( 'synced', function() { self.container.removeClass( 'previewer-loading' ); } ); api.previewer.bind( 'widget-updated', function( updatedWidgetId ) { if ( updatedWidgetId === self.params.widget_id ) { self.container.removeClass( 'previewer-loading' ); } } ); formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ]; if ( formSyncHandler ) { $( document ).on( 'widget-synced', function( e, widget ) { if ( $widgetRoot.is( widget ) ) { formSyncHandler.apply( document, arguments ); } } ); } }, /** * Update widget control to indicate whether it is currently rendered. * * Overrides api.Control.toggle() * * @since 4.1.0 * * @param {boolean} active * @param {Object} args * @param {function} args.completeCallback */ onChangeActive: function ( active, args ) { // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments. this.container.toggleClass( 'widget-rendered', active ); if ( args.completeCallback ) { args.completeCallback(); } }, /** * Set up event handlers for widget removal */ _setupRemoveUI: function() { var self = this, $removeBtn, replaceDeleteWithRemove; // Configure remove button. $removeBtn = this.container.find( '.widget-control-remove' ); $removeBtn.on( 'click', function() { // Find an adjacent element to add focus to when this widget goes away. var $adjacentFocusTarget; if ( self.container.next().is( '.customize-control-widget_form' ) ) { $adjacentFocusTarget = self.container.next().find( '.widget-action:first' ); } else if ( self.container.prev().is( '.customize-control-widget_form' ) ) { $adjacentFocusTarget = self.container.prev().find( '.widget-action:first' ); } else { $adjacentFocusTarget = self.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' ); } self.container.slideUp( function() { var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ), sidebarWidgetIds, i; if ( ! sidebarsWidgetsControl ) { return; } sidebarWidgetIds = sidebarsWidgetsControl.setting().slice(); i = _.indexOf( sidebarWidgetIds, self.params.widget_id ); if ( -1 === i ) { return; } sidebarWidgetIds.splice( i, 1 ); sidebarsWidgetsControl.setting( sidebarWidgetIds ); $adjacentFocusTarget.focus(); // Keyboard accessibility. } ); } ); replaceDeleteWithRemove = function() { $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the button as "Delete". $removeBtn.attr( 'title', l10n.removeBtnTooltip ); }; if ( this.params.is_new ) { api.bind( 'saved', replaceDeleteWithRemove ); } else { replaceDeleteWithRemove(); } }, /** * Find all inputs in a widget container that should be considered when * comparing the loaded form with the sanitized form, whose fields will * be aligned to copy the sanitized over. The elements returned by this * are passed into this._getInputsSignature(), and they are iterated * over when copying sanitized values over to the form loaded. * * @param {jQuery} container element in which to look for inputs * @return {jQuery} inputs * @private */ _getInputs: function( container ) { return $( container ).find( ':input[name]' ); }, /** * Iterate over supplied inputs and create a signature string for all of them together. * This string can be used to compare whether or not the form has all of the same fields. * * @param {jQuery} inputs * @return {string} * @private */ _getInputsSignature: function( inputs ) { var inputsSignatures = _( inputs ).map( function( input ) { var $input = $( input ), signatureParts; if ( $input.is( ':checkbox, :radio' ) ) { signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ), $input.prop( 'value' ) ]; } else { signatureParts = [ $input.attr( 'id' ), $input.attr( 'name' ) ]; } return signatureParts.join( ',' ); } ); return inputsSignatures.join( ';' ); }, /** * Get the state for an input depending on its type. * * @param {jQuery|Element} input * @return {string|boolean|Array|*} * @private */ _getInputState: function( input ) { input = $( input ); if ( input.is( ':radio, :checkbox' ) ) { return input.prop( 'checked' ); } else if ( input.is( 'select[multiple]' ) ) { return input.find( 'option:selected' ).map( function () { return $( this ).val(); } ).get(); } else { return input.val(); } }, /** * Update an input's state based on its type. * * @param {jQuery|Element} input * @param {string|boolean|Array|*} state * @private */ _setInputState: function ( input, state ) { input = $( input ); if ( input.is( ':radio, :checkbox' ) ) { input.prop( 'checked', state ); } else if ( input.is( 'select[multiple]' ) ) { if ( ! Array.isArray( state ) ) { state = []; } else { // Make sure all state items are strings since the DOM value is a string. state = _.map( state, function ( value ) { return String( value ); } ); } input.find( 'option' ).each( function () { $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) ); } ); } else { input.val( state ); } }, /*********************************************************************** * Begin public API methods **********************************************************************/ /** * @return {wp.customize.controlConstructor.sidebar_widgets[]} */ getSidebarWidgetsControl: function() { var settingId, sidebarWidgetsControl; settingId = 'sidebars_widgets[' + this.params.sidebar_id + ']'; sidebarWidgetsControl = api.control( settingId ); if ( ! sidebarWidgetsControl ) { return; } return sidebarWidgetsControl; }, /** * Submit the widget form via Ajax and get back the updated instance, * along with the new widget control form to render. * * @param {Object} [args] * @param {Object|null} [args.instance=null] When the model changes, the instance is sent here; otherwise, the inputs from the form are used * @param {Function|null} [args.complete=null] Function which is called when the request finishes. Context is bound to the control. First argument is any error. Following arguments are for success. * @param {boolean} [args.ignoreActiveElement=false] Whether or not updating a field will be deferred if focus is still on the element. */ updateWidget: function( args ) { var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent, updateNumber, params, data, $inputs, processing, jqxhr, isChanged; // The updateWidget logic requires that the form fields to be fully present. self.embedWidgetContent(); args = $.extend( { instance: null, complete: null, ignoreActiveElement: false }, args ); instanceOverride = args.instance; completeCallback = args.complete; this._updateCount += 1; updateNumber = this._updateCount; $widgetRoot = this.container.find( '.widget:first' ); $widgetContent = $widgetRoot.find( '.widget-content:first' ); // Remove a previous error message. $widgetContent.find( '.widget-error' ).remove(); this.container.addClass( 'widget-form-loading' ); this.container.addClass( 'previewer-loading' ); processing = api.state( 'processing' ); processing( processing() + 1 ); if ( ! this.liveUpdateMode ) { this.container.addClass( 'widget-form-disabled' ); } params = {}; params.action = 'update-widget'; params.wp_customize = 'on'; params.nonce = api.settings.nonce['update-widget']; params.customize_theme = api.settings.theme.stylesheet; params.customized = wp.customize.previewer.query().customized; data = $.param( params ); $inputs = this._getInputs( $widgetContent ); /* * Store the value we're submitting in data so that when the response comes back, * we know if it got sanitized; if there is no difference in the sanitized value, * then we do not need to touch the UI and mess up the user's ongoing editing. */ $inputs.each( function() { $( this ).data( 'state' + updateNumber, self._getInputState( this ) ); } ); if ( instanceOverride ) { data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } ); } else { data += '&' + $inputs.serialize(); } data += '&' + $widgetContent.find( '~ :input' ).serialize(); if ( this._previousUpdateRequest ) { this._previousUpdateRequest.abort(); } jqxhr = $.post( wp.ajax.settings.url, data ); this._previousUpdateRequest = jqxhr; jqxhr.done( function( r ) { var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse, isLiveUpdateAborted = false; // Check if the user is logged out. if ( '0' === r ) { api.previewer.preview.iframe.hide(); api.previewer.login().done( function() { self.updateWidget( args ); api.previewer.preview.iframe.show(); } ); return; } // Check for cheaters. if ( '-1' === r ) { api.previewer.cheatin(); return; } if ( r.success ) { sanitizedForm = $( '
    ' + r.data.form + '
    ' ); $sanitizedInputs = self._getInputs( sanitizedForm ); hasSameInputsInResponse = self._getInputsSignature( $inputs ) === self._getInputsSignature( $sanitizedInputs ); // Restore live update mode if sanitized fields are now aligned with the existing fields. if ( hasSameInputsInResponse && ! self.liveUpdateMode ) { self.liveUpdateMode = true; self.container.removeClass( 'widget-form-disabled' ); self.container.find( 'input[name="savewidget"]' ).hide(); } // Sync sanitized field states to existing fields if they are aligned. if ( hasSameInputsInResponse && self.liveUpdateMode ) { $inputs.each( function( i ) { var $input = $( this ), $sanitizedInput = $( $sanitizedInputs[i] ), submittedState, sanitizedState, canUpdateState; submittedState = $input.data( 'state' + updateNumber ); sanitizedState = self._getInputState( $sanitizedInput ); $input.data( 'sanitized', sanitizedState ); canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) ); if ( canUpdateState ) { self._setInputState( $input, sanitizedState ); } } ); $( document ).trigger( 'widget-synced', [ $widgetRoot, r.data.form ] ); // Otherwise, if sanitized fields are not aligned with existing fields, disable live update mode if enabled. } else if ( self.liveUpdateMode ) { self.liveUpdateMode = false; self.container.find( 'input[name="savewidget"]' ).show(); isLiveUpdateAborted = true; // Otherwise, replace existing form with the sanitized form. } else { $widgetContent.html( r.data.form ); self.container.removeClass( 'widget-form-disabled' ); $( document ).trigger( 'widget-updated', [ $widgetRoot ] ); } /** * If the old instance is identical to the new one, there is nothing new * needing to be rendered, and so we can preempt the event for the * preview finishing loading. */ isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance ); if ( isChanged ) { self.isWidgetUpdating = true; // Suppress triggering another updateWidget. self.setting( r.data.instance ); self.isWidgetUpdating = false; } else { // No change was made, so stop the spinner now instead of when the preview would updates. self.container.removeClass( 'previewer-loading' ); } if ( completeCallback ) { completeCallback.call( self, null, { noChange: ! isChanged, ajaxFinished: true } ); } } else { // General error message. message = l10n.error; if ( r.data && r.data.message ) { message = r.data.message; } if ( completeCallback ) { completeCallback.call( self, message ); } else { $widgetContent.prepend( '

    ' + message + '

    ' ); } } } ); jqxhr.fail( function( jqXHR, textStatus ) { if ( completeCallback ) { completeCallback.call( self, textStatus ); } } ); jqxhr.always( function() { self.container.removeClass( 'widget-form-loading' ); $inputs.each( function() { $( this ).removeData( 'state' + updateNumber ); } ); processing( processing() - 1 ); } ); }, /** * Expand the accordion section containing a control */ expandControlSection: function() { api.Control.prototype.expand.call( this ); }, /** * @since 4.1.0 * * @param {Boolean} expanded * @param {Object} [params] * @return {Boolean} False if state already applied. */ _toggleExpanded: api.Section.prototype._toggleExpanded, /** * @since 4.1.0 * * @param {Object} [params] * @return {Boolean} False if already expanded. */ expand: api.Section.prototype.expand, /** * Expand the widget form control * * @deprecated 4.1.0 Use this.expand() instead. */ expandForm: function() { this.expand(); }, /** * @since 4.1.0 * * @param {Object} [params] * @return {Boolean} False if already collapsed. */ collapse: api.Section.prototype.collapse, /** * Collapse the widget form control * * @deprecated 4.1.0 Use this.collapse() instead. */ collapseForm: function() { this.collapse(); }, /** * Expand or collapse the widget control * * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) * * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility */ toggleForm: function( showOrHide ) { if ( typeof showOrHide === 'undefined' ) { showOrHide = ! this.expanded(); } this.expanded( showOrHide ); }, /** * Respond to change in the expanded state. * * @param {boolean} expanded * @param {Object} args merged on top of this.defaultActiveArguments */ onChangeExpanded: function ( expanded, args ) { var self = this, $widget, $inside, complete, prevComplete, expandControl, $toggleBtn; self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. if ( expanded ) { self.embedWidgetContent(); } // If the expanded state is unchanged only manipulate container expanded states. if ( args.unchanged ) { if ( expanded ) { api.Control.prototype.expand.call( self, { completeCallback: args.completeCallback }); } return; } $widget = this.container.find( 'div.widget:first' ); $inside = $widget.find( '.widget-inside:first' ); $toggleBtn = this.container.find( '.widget-top button.widget-action' ); expandControl = function() { // Close all other widget controls before expanding this one. api.control.each( function( otherControl ) { if ( self.params.type === otherControl.params.type && self !== otherControl ) { otherControl.collapse(); } } ); complete = function() { self.container.removeClass( 'expanding' ); self.container.addClass( 'expanded' ); $widget.addClass( 'open' ); $toggleBtn.attr( 'aria-expanded', 'true' ); self.container.trigger( 'expanded' ); }; if ( args.completeCallback ) { prevComplete = complete; complete = function () { prevComplete(); args.completeCallback(); }; } if ( self.params.is_wide ) { $inside.fadeIn( args.duration, complete ); } else { $inside.slideDown( args.duration, complete ); } self.container.trigger( 'expand' ); self.container.addClass( 'expanding' ); }; if ( $toggleBtn.attr( 'aria-expanded' ) === 'false' ) { if ( api.section.has( self.section() ) ) { api.section( self.section() ).expand( { completeCallback: expandControl } ); } else { expandControl(); } } else { complete = function() { self.container.removeClass( 'collapsing' ); self.container.removeClass( 'expanded' ); $widget.removeClass( 'open' ); $toggleBtn.attr( 'aria-expanded', 'false' ); self.container.trigger( 'collapsed' ); }; if ( args.completeCallback ) { prevComplete = complete; complete = function () { prevComplete(); args.completeCallback(); }; } self.container.trigger( 'collapse' ); self.container.addClass( 'collapsing' ); if ( self.params.is_wide ) { $inside.fadeOut( args.duration, complete ); } else { $inside.slideUp( args.duration, function() { $widget.css( { width:'', margin:'' } ); complete(); } ); } } }, /** * Get the position (index) of the widget in the containing sidebar * * @return {number} */ getWidgetSidebarPosition: function() { var sidebarWidgetIds, position; sidebarWidgetIds = this.getSidebarWidgetsControl().setting(); position = _.indexOf( sidebarWidgetIds, this.params.widget_id ); if ( position === -1 ) { return; } return position; }, /** * Move widget up one in the sidebar */ moveUp: function() { this._moveWidgetByOne( -1 ); }, /** * Move widget up one in the sidebar */ moveDown: function() { this._moveWidgetByOne( 1 ); }, /** * @private * * @param {number} offset 1|-1 */ _moveWidgetByOne: function( offset ) { var i, sidebarWidgetsSetting, sidebarWidgetIds, adjacentWidgetId; i = this.getWidgetSidebarPosition(); sidebarWidgetsSetting = this.getSidebarWidgetsControl().setting; sidebarWidgetIds = Array.prototype.slice.call( sidebarWidgetsSetting() ); // Clone. adjacentWidgetId = sidebarWidgetIds[i + offset]; sidebarWidgetIds[i + offset] = this.params.widget_id; sidebarWidgetIds[i] = adjacentWidgetId; sidebarWidgetsSetting( sidebarWidgetIds ); }, /** * Toggle visibility of the widget move area * * @param {boolean} [showOrHide] */ toggleWidgetMoveArea: function( showOrHide ) { var self = this, $moveWidgetArea; $moveWidgetArea = this.container.find( '.move-widget-area' ); if ( typeof showOrHide === 'undefined' ) { showOrHide = ! $moveWidgetArea.hasClass( 'active' ); } if ( showOrHide ) { // Reset the selected sidebar. $moveWidgetArea.find( '.selected' ).removeClass( 'selected' ); $moveWidgetArea.find( 'li' ).filter( function() { return $( this ).data( 'id' ) === self.params.sidebar_id; } ).addClass( 'selected' ); this.container.find( '.move-widget-btn' ).prop( 'disabled', true ); } $moveWidgetArea.toggleClass( 'active', showOrHide ); }, /** * Highlight the widget control and section */ highlightSectionAndControl: function() { var $target; if ( this.container.is( ':hidden' ) ) { $target = this.container.closest( '.control-section' ); } else { $target = this.container; } $( '.highlighted' ).removeClass( 'highlighted' ); $target.addClass( 'highlighted' ); setTimeout( function() { $target.removeClass( 'highlighted' ); }, 500 ); } } ); /** * wp.customize.Widgets.WidgetsPanel * * Customizer panel containing the widget area sections. * * @since 4.4.0 * * @class wp.customize.Widgets.WidgetsPanel * @augments wp.customize.Panel */ api.Widgets.WidgetsPanel = api.Panel.extend(/** @lends wp.customize.Widgets.WigetsPanel.prototype */{ /** * Add and manage the display of the no-rendered-areas notice. * * @since 4.4.0 */ ready: function () { var panel = this; api.Panel.prototype.ready.call( panel ); panel.deferred.embedded.done(function() { var panelMetaContainer, noticeContainer, updateNotice, getActiveSectionCount, shouldShowNotice; panelMetaContainer = panel.container.find( '.panel-meta' ); // @todo This should use the Notifications API introduced to panels. See . noticeContainer = $( '
    ', { 'class': 'no-widget-areas-rendered-notice', 'role': 'alert' }); panelMetaContainer.append( noticeContainer ); /** * Get the number of active sections in the panel. * * @return {number} Number of active sidebar sections. */ getActiveSectionCount = function() { return _.filter( panel.sections(), function( section ) { return 'sidebar' === section.params.type && section.active(); } ).length; }; /** * Determine whether or not the notice should be displayed. * * @return {boolean} */ shouldShowNotice = function() { var activeSectionCount = getActiveSectionCount(); if ( 0 === activeSectionCount ) { return true; } else { return activeSectionCount !== api.Widgets.data.registeredSidebars.length; } }; /** * Update the notice. * * @return {void} */ updateNotice = function() { var activeSectionCount = getActiveSectionCount(), someRenderedMessage, nonRenderedAreaCount, registeredAreaCount; noticeContainer.empty(); registeredAreaCount = api.Widgets.data.registeredSidebars.length; if ( activeSectionCount !== registeredAreaCount ) { if ( 0 !== activeSectionCount ) { nonRenderedAreaCount = registeredAreaCount - activeSectionCount; someRenderedMessage = l10n.someAreasShown[ nonRenderedAreaCount ]; } else { someRenderedMessage = l10n.noAreasShown; } if ( someRenderedMessage ) { noticeContainer.append( $( '

    ', { text: someRenderedMessage } ) ); } noticeContainer.append( $( '

    ', { text: l10n.navigatePreview } ) ); } }; updateNotice(); /* * Set the initial visibility state for rendered notice. * Update the visibility of the notice whenever a reflow happens. */ noticeContainer.toggle( shouldShowNotice() ); api.previewer.deferred.active.done( function () { noticeContainer.toggle( shouldShowNotice() ); }); api.bind( 'pane-contents-reflowed', function() { var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0; updateNotice(); if ( shouldShowNotice() ) { noticeContainer.slideDown( duration ); } else { noticeContainer.slideUp( duration ); } }); }); }, /** * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas). * * This ensures that the widgets panel appears even when there are no * sidebars displayed on the URL currently being previewed. * * @since 4.4.0 * * @return {boolean} */ isContextuallyActive: function() { var panel = this; return panel.active(); } }); /** * wp.customize.Widgets.SidebarSection * * Customizer section representing a widget area widget * * @since 4.1.0 * * @class wp.customize.Widgets.SidebarSection * @augments wp.customize.Section */ api.Widgets.SidebarSection = api.Section.extend(/** @lends wp.customize.Widgets.SidebarSection.prototype */{ /** * Sync the section's active state back to the Backbone model's is_rendered attribute * * @since 4.1.0 */ ready: function () { var section = this, registeredSidebar; api.Section.prototype.ready.call( this ); registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId ); section.active.bind( function ( active ) { registeredSidebar.set( 'is_rendered', active ); }); registeredSidebar.set( 'is_rendered', section.active() ); } }); /** * wp.customize.Widgets.SidebarControl * * Customizer control for widgets. * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type * * @since 3.9.0 * * @class wp.customize.Widgets.SidebarControl * @augments wp.customize.Control */ api.Widgets.SidebarControl = api.Control.extend(/** @lends wp.customize.Widgets.SidebarControl.prototype */{ /** * Set up the control */ ready: function() { this.$controlSection = this.container.closest( '.control-section' ); this.$sectionContent = this.container.closest( '.accordion-section-content' ); this._setupModel(); this._setupSortable(); this._setupAddition(); this._applyCardinalOrderClassNames(); }, /** * Update ordering of widget control forms when the setting is updated */ _setupModel: function() { var self = this; this.setting.bind( function( newWidgetIds, oldWidgetIds ) { var widgetFormControls, removedWidgetIds, priority; removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); // Filter out any persistent widget IDs for widgets which have been deactivated. newWidgetIds = _( newWidgetIds ).filter( function( newWidgetId ) { var parsedWidgetId = parseWidgetId( newWidgetId ); return !! api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } ); } ); widgetFormControls = _( newWidgetIds ).map( function( widgetId ) { var widgetFormControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); if ( ! widgetFormControl ) { widgetFormControl = self.addWidget( widgetId ); } return widgetFormControl; } ); // Sort widget controls to their new positions. widgetFormControls.sort( function( a, b ) { var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); return aIndex - bIndex; }); priority = 0; _( widgetFormControls ).each( function ( control ) { control.priority( priority ); control.section( self.section() ); priority += 1; }); self.priority( priority ); // Make sure sidebar control remains at end. // Re-sort widget form controls (including widgets form other sidebars newly moved here). self._applyCardinalOrderClassNames(); // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated. _( widgetFormControls ).each( function( widgetFormControl ) { widgetFormControl.params.sidebar_id = self.params.sidebar_id; } ); // Cleanup after widget removal. _( removedWidgetIds ).each( function( removedWidgetId ) { // Using setTimeout so that when moving a widget to another sidebar, // the other sidebars_widgets settings get a chance to update. setTimeout( function() { var removedControl, wasDraggedToAnotherSidebar, inactiveWidgets, removedIdBase, widget, isPresentInAnotherSidebar = false; // Check if the widget is in another sidebar. api.each( function( otherSetting ) { if ( otherSetting.id === self.setting.id || 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) || otherSetting.id === 'sidebars_widgets[wp_inactive_widgets]' ) { return; } var otherSidebarWidgets = otherSetting(), i; i = _.indexOf( otherSidebarWidgets, removedWidgetId ); if ( -1 !== i ) { isPresentInAnotherSidebar = true; } } ); // If the widget is present in another sidebar, abort! if ( isPresentInAnotherSidebar ) { return; } removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId ); // Detect if widget control was dragged to another sidebar. wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] ); // Delete any widget form controls for removed widgets. if ( removedControl && ! wasDraggedToAnotherSidebar ) { api.control.remove( removedControl.id ); removedControl.container.remove(); } // Move widget to inactive widgets sidebar (move it to Trash) if has been previously saved. // This prevents the inactive widgets sidebar from overflowing with throwaway widgets. if ( api.Widgets.savedWidgetIds[removedWidgetId] ) { inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice(); inactiveWidgets.push( removedWidgetId ); api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() ); } // Make old single widget available for adding again. removedIdBase = parseWidgetId( removedWidgetId ).id_base; widget = api.Widgets.availableWidgets.findWhere( { id_base: removedIdBase } ); if ( widget && ! widget.get( 'is_multi' ) ) { widget.set( 'is_disabled', false ); } } ); } ); } ); }, /** * Allow widgets in sidebar to be re-ordered, and for the order to be previewed */ _setupSortable: function() { var self = this; this.isReordering = false; /** * Update widget order setting when controls are re-ordered */ this.$sectionContent.sortable( { items: '> .customize-control-widget_form', handle: '.widget-top', axis: 'y', tolerance: 'pointer', connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)', update: function() { var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds; widgetIds = $.map( widgetContainerIds, function( widgetContainerId ) { return $( '#' + widgetContainerId ).find( ':input[name=widget-id]' ).val(); } ); self.setting( widgetIds ); } } ); /** * Expand other Customizer sidebar section when dragging a control widget over it, * allowing the control to be dropped into another section */ this.$controlSection.find( '.accordion-section-title' ).droppable({ accept: '.customize-control-widget_form', over: function() { var section = api.section( self.section.get() ); section.expand({ allowMultiple: true, // Prevent the section being dragged from to be collapsed. completeCallback: function () { // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed. api.section.each( function ( otherSection ) { if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); } } ); } }); } }); /** * Keyboard-accessible reordering */ this.container.find( '.reorder-toggle' ).on( 'click', function() { self.toggleReordering( ! self.isReordering ); } ); }, /** * Set up UI for adding a new widget */ _setupAddition: function() { var self = this; this.container.find( '.add-new-widget' ).on( 'click', function() { var addNewWidgetBtn = $( this ); if ( self.$sectionContent.hasClass( 'reordering' ) ) { return; } if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) { addNewWidgetBtn.attr( 'aria-expanded', 'true' ); api.Widgets.availableWidgetsPanel.open( self ); } else { addNewWidgetBtn.attr( 'aria-expanded', 'false' ); api.Widgets.availableWidgetsPanel.close(); } } ); }, /** * Add classes to the widget_form controls to assist with styling */ _applyCardinalOrderClassNames: function() { var widgetControls = []; _.each( this.setting(), function ( widgetId ) { var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); if ( widgetControl ) { widgetControls.push( widgetControl ); } }); if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) { this.container.find( '.reorder-toggle' ).hide(); return; } else { this.container.find( '.reorder-toggle' ).show(); } $( widgetControls ).each( function () { $( this.container ) .removeClass( 'first-widget' ) .removeClass( 'last-widget' ) .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); }); _.first( widgetControls ).container .addClass( 'first-widget' ) .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); _.last( widgetControls ).container .addClass( 'last-widget' ) .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); }, /*********************************************************************** * Begin public API methods **********************************************************************/ /** * Enable/disable the reordering UI * * @param {boolean} showOrHide to enable/disable reordering * * @todo We should have a reordering state instead and rename this to onChangeReordering */ toggleReordering: function( showOrHide ) { var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ), reorderBtn = this.container.find( '.reorder-toggle' ), widgetsTitle = this.$sectionContent.find( '.widget-title' ); showOrHide = Boolean( showOrHide ); if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) { return; } this.isReordering = showOrHide; this.$sectionContent.toggleClass( 'reordering', showOrHide ); if ( showOrHide ) { _( this.getWidgetFormControls() ).each( function( formControl ) { formControl.collapse(); } ); addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); reorderBtn.attr( 'aria-label', l10n.reorderLabelOff ); wp.a11y.speak( l10n.reorderModeOn ); // Hide widget titles while reordering: title is already in the reorder controls. widgetsTitle.attr( 'aria-hidden', 'true' ); } else { addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' ); reorderBtn.attr( 'aria-label', l10n.reorderLabelOn ); wp.a11y.speak( l10n.reorderModeOff ); widgetsTitle.attr( 'aria-hidden', 'false' ); } }, /** * Get the widget_form Customize controls associated with the current sidebar. * * @since 3.9.0 * @return {wp.customize.controlConstructor.widget_form[]} */ getWidgetFormControls: function() { var formControls = []; _( this.setting() ).each( function( widgetId ) { var settingId = widgetIdToSettingId( widgetId ), formControl = api.control( settingId ); if ( formControl ) { formControls.push( formControl ); } } ); return formControls; }, /** * @param {string} widgetId or an id_base for adding a previously non-existing widget. * @return {Object|false} widget_form control instance, or false on error. */ addWidget: function( widgetId ) { var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, parsedWidgetId = parseWidgetId( widgetId ), widgetNumber = parsedWidgetId.number, widgetIdBase = parsedWidgetId.id_base, widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ), settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting; if ( ! widget ) { return false; } if ( widgetNumber && ! widget.get( 'is_multi' ) ) { return false; } // Set up new multi widget. if ( widget.get( 'is_multi' ) && ! widgetNumber ) { widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 ); widgetNumber = widget.get( 'multi_number' ); } controlHtml = $( '#widget-tpl-' + widget.get( 'id' ) ).html().trim(); if ( widget.get( 'is_multi' ) ) { controlHtml = controlHtml.replace( /<[^<>]+>/g, function( m ) { return m.replace( /__i__|%i%/g, widgetNumber ); } ); } else { widget.set( 'is_disabled', true ); // Prevent single widget from being added again now. } $widget = $( controlHtml ); controlContainer = $( '
  • ' ) .addClass( 'customize-control' ) .addClass( 'customize-control-' + controlType ) .append( $widget ); // Remove icon which is visible inside the panel. controlContainer.find( '> .widget-icon' ).remove(); if ( widget.get( 'is_multi' ) ) { controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); } widgetId = controlContainer.find( '[name="widget-id"]' ).val(); controlContainer.hide(); // To be slid-down below. settingId = 'widget_' + widget.get( 'id_base' ); if ( widget.get( 'is_multi' ) ) { settingId += '[' + widgetNumber + ']'; } controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget). isExistingWidget = api.has( settingId ); if ( ! isExistingWidget ) { settingArgs = { transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh', previewer: this.setting.previewer }; setting = api.create( settingId, settingId, '', settingArgs ); setting.set( {} ); // Mark dirty, changing from '' to {}. } controlConstructor = api.controlConstructor[controlType]; widgetFormControl = new controlConstructor( settingId, { settings: { 'default': settingId }, content: controlContainer, sidebar_id: self.params.sidebar_id, widget_id: widgetId, widget_id_base: widget.get( 'id_base' ), type: controlType, is_new: ! isExistingWidget, width: widget.get( 'width' ), height: widget.get( 'height' ), is_wide: widget.get( 'is_wide' ) } ); api.control.add( widgetFormControl ); // Make sure widget is removed from the other sidebars. api.each( function( otherSetting ) { if ( otherSetting.id === self.setting.id ) { return; } if ( 0 !== otherSetting.id.indexOf( 'sidebars_widgets[' ) ) { return; } var otherSidebarWidgets = otherSetting().slice(), i = _.indexOf( otherSidebarWidgets, widgetId ); if ( -1 !== i ) { otherSidebarWidgets.splice( i ); otherSetting( otherSidebarWidgets ); } } ); // Add widget to this sidebar. sidebarWidgets = this.setting().slice(); if ( -1 === _.indexOf( sidebarWidgets, widgetId ) ) { sidebarWidgets.push( widgetId ); this.setting( sidebarWidgets ); } controlContainer.slideDown( function() { if ( isExistingWidget ) { widgetFormControl.updateWidget( { instance: widgetFormControl.setting() } ); } } ); return widgetFormControl; } } ); // Register models for custom panel, section, and control types. $.extend( api.panelConstructor, { widgets: api.Widgets.WidgetsPanel }); $.extend( api.sectionConstructor, { sidebar: api.Widgets.SidebarSection }); $.extend( api.controlConstructor, { widget_form: api.Widgets.WidgetControl, sidebar_widgets: api.Widgets.SidebarControl }); /** * Init Customizer for widgets. */ api.bind( 'ready', function() { // Set up the widgets panel. api.Widgets.availableWidgetsPanel = new api.Widgets.AvailableWidgetsPanelView({ collection: api.Widgets.availableWidgets }); // Highlight widget control. api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); // Open and focus widget control. api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); } ); /** * Highlight a widget control. * * @param {string} widgetId */ api.Widgets.highlightWidgetFormControl = function( widgetId ) { var control = api.Widgets.getWidgetFormControlForWidget( widgetId ); if ( control ) { control.highlightSectionAndControl(); } }, /** * Focus a widget control. * * @param {string} widgetId */ api.Widgets.focusWidgetFormControl = function( widgetId ) { var control = api.Widgets.getWidgetFormControlForWidget( widgetId ); if ( control ) { control.focus(); } }, /** * Given a widget control, find the sidebar widgets control that contains it. * @param {string} widgetId * @return {Object|null} */ api.Widgets.getSidebarWidgetControlContainingWidget = function( widgetId ) { var foundControl = null; // @todo This can use widgetIdToSettingId(), then pass into wp.customize.control( x ).getSidebarWidgetsControl(). api.control.each( function( control ) { if ( control.params.type === 'sidebar_widgets' && -1 !== _.indexOf( control.setting(), widgetId ) ) { foundControl = control; } } ); return foundControl; }; /** * Given a widget ID for a widget appearing in the preview, get the widget form control associated with it. * * @param {string} widgetId * @return {Object|null} */ api.Widgets.getWidgetFormControlForWidget = function( widgetId ) { var foundControl = null; // @todo We can just use widgetIdToSettingId() here. api.control.each( function( control ) { if ( control.params.type === 'widget_form' && control.params.widget_id === widgetId ) { foundControl = control; } } ); return foundControl; }; /** * Initialize Edit Menu button in Nav Menu widget. */ $( document ).on( 'widget-added', function( event, widgetContainer ) { var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton; parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() ); if ( 'nav_menu' !== parsedWidgetId.id_base ) { return; } widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' ); if ( ! widgetControl ) { return; } navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' ); editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' ); if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) { return; } navMenuSelect.on( 'change', function() { if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) { editMenuButton.parent().show(); } else { editMenuButton.parent().hide(); } }); editMenuButton.on( 'click', function() { var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' ); if ( section ) { focusConstructWithBreadcrumb( section, widgetControl ); } } ); } ); /** * Focus (expand) one construct and then focus on another construct after the first is collapsed. * * This overrides the back button to serve the purpose of breadcrumb navigation. * * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus. * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus. */ function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) { focusConstruct.focus(); function onceCollapsed( isExpanded ) { if ( ! isExpanded ) { focusConstruct.expanded.unbind( onceCollapsed ); returnConstruct.focus(); } } focusConstruct.expanded.bind( onceCollapsed ); } /** * @param {string} widgetId * @return {Object} */ function parseWidgetId( widgetId ) { var matches, parsed = { number: null, id_base: null }; matches = widgetId.match( /^(.+)-(\d+)$/ ); if ( matches ) { parsed.id_base = matches[1]; parsed.number = parseInt( matches[2], 10 ); } else { // Likely an old single widget. parsed.id_base = widgetId; } return parsed; } /** * @param {string} widgetId * @return {string} settingId */ function widgetIdToSettingId( widgetId ) { var parsed = parseWidgetId( widgetId ), settingId; settingId = 'widget_' + parsed.id_base; if ( parsed.number ) { settingId += '[' + parsed.number + ']'; } return settingId; } })( window.wp, jQuery ); PK1YZiD-D-code-editor.jsnu[/** * @output wp-admin/js/code-editor.js */ if ( 'undefined' === typeof window.wp ) { /** * @namespace wp */ window.wp = {}; } if ( 'undefined' === typeof window.wp.codeEditor ) { /** * @namespace wp.codeEditor */ window.wp.codeEditor = {}; } ( function( $, wp ) { 'use strict'; /** * Default settings for code editor. * * @since 4.9.0 * @type {object} */ wp.codeEditor.defaultSettings = { codemirror: {}, csslint: {}, htmlhint: {}, jshint: {}, onTabNext: function() {}, onTabPrevious: function() {}, onChangeLintingErrors: function() {}, onUpdateErrorNotice: function() {} }; /** * Configure linting. * * @param {CodeMirror} editor - Editor. * @param {Object} settings - Code editor settings. * @param {Object} settings.codeMirror - Settings for CodeMirror. * @param {Function} settings.onChangeLintingErrors - Callback for when there are changes to linting errors. * @param {Function} settings.onUpdateErrorNotice - Callback to update error notice. * * @return {void} */ function configureLinting( editor, settings ) { // eslint-disable-line complexity var currentErrorAnnotations = [], previouslyShownErrorAnnotations = []; /** * Call the onUpdateErrorNotice if there are new errors to show. * * @return {void} */ function updateErrorNotice() { if ( settings.onUpdateErrorNotice && ! _.isEqual( currentErrorAnnotations, previouslyShownErrorAnnotations ) ) { settings.onUpdateErrorNotice( currentErrorAnnotations, editor ); previouslyShownErrorAnnotations = currentErrorAnnotations; } } /** * Get lint options. * * @return {Object} Lint options. */ function getLintOptions() { // eslint-disable-line complexity var options = editor.getOption( 'lint' ); if ( ! options ) { return false; } if ( true === options ) { options = {}; } else if ( _.isObject( options ) ) { options = $.extend( {}, options ); } /* * Note that rules must be sent in the "deprecated" lint.options property * to prevent linter from complaining about unrecognized options. * See . */ if ( ! options.options ) { options.options = {}; } // Configure JSHint. if ( 'javascript' === settings.codemirror.mode && settings.jshint ) { $.extend( options.options, settings.jshint ); } // Configure CSSLint. if ( 'css' === settings.codemirror.mode && settings.csslint ) { $.extend( options.options, settings.csslint ); } // Configure HTMLHint. if ( 'htmlmixed' === settings.codemirror.mode && settings.htmlhint ) { options.options.rules = $.extend( {}, settings.htmlhint ); if ( settings.jshint ) { options.options.rules.jshint = settings.jshint; } if ( settings.csslint ) { options.options.rules.csslint = settings.csslint; } } // Wrap the onUpdateLinting CodeMirror event to route to onChangeLintingErrors and onUpdateErrorNotice. options.onUpdateLinting = (function( onUpdateLintingOverridden ) { return function( annotations, annotationsSorted, cm ) { var errorAnnotations = _.filter( annotations, function( annotation ) { return 'error' === annotation.severity; } ); if ( onUpdateLintingOverridden ) { onUpdateLintingOverridden.apply( annotations, annotationsSorted, cm ); } // Skip if there are no changes to the errors. if ( _.isEqual( errorAnnotations, currentErrorAnnotations ) ) { return; } currentErrorAnnotations = errorAnnotations; if ( settings.onChangeLintingErrors ) { settings.onChangeLintingErrors( errorAnnotations, annotations, annotationsSorted, cm ); } /* * Update notifications when the editor is not focused to prevent error message * from overwhelming the user during input, unless there are now no errors or there * were previously errors shown. In these cases, update immediately so they can know * that they fixed the errors. */ if ( ! editor.state.focused || 0 === currentErrorAnnotations.length || previouslyShownErrorAnnotations.length > 0 ) { updateErrorNotice(); } }; })( options.onUpdateLinting ); return options; } editor.setOption( 'lint', getLintOptions() ); // Keep lint options populated. editor.on( 'optionChange', function( cm, option ) { var options, gutters, gutterName = 'CodeMirror-lint-markers'; if ( 'lint' !== option ) { return; } gutters = editor.getOption( 'gutters' ) || []; options = editor.getOption( 'lint' ); if ( true === options ) { if ( ! _.contains( gutters, gutterName ) ) { editor.setOption( 'gutters', [ gutterName ].concat( gutters ) ); } editor.setOption( 'lint', getLintOptions() ); // Expand to include linting options. } else if ( ! options ) { editor.setOption( 'gutters', _.without( gutters, gutterName ) ); } // Force update on error notice to show or hide. if ( editor.getOption( 'lint' ) ) { editor.performLint(); } else { currentErrorAnnotations = []; updateErrorNotice(); } } ); // Update error notice when leaving the editor. editor.on( 'blur', updateErrorNotice ); // Work around hint selection with mouse causing focus to leave editor. editor.on( 'startCompletion', function() { editor.off( 'blur', updateErrorNotice ); } ); editor.on( 'endCompletion', function() { var editorRefocusWait = 500; editor.on( 'blur', updateErrorNotice ); // Wait for editor to possibly get re-focused after selection. _.delay( function() { if ( ! editor.state.focused ) { updateErrorNotice(); } }, editorRefocusWait ); }); /* * Make sure setting validities are set if the user tries to click Publish * while an autocomplete dropdown is still open. The Customizer will block * saving when a setting has an error notifications on it. This is only * necessary for mouse interactions because keyboards will have already * blurred the field and cause onUpdateErrorNotice to have already been * called. */ $( document.body ).on( 'mousedown', function( event ) { if ( editor.state.focused && ! $.contains( editor.display.wrapper, event.target ) && ! $( event.target ).hasClass( 'CodeMirror-hint' ) ) { updateErrorNotice(); } }); } /** * Configure tabbing. * * @param {CodeMirror} codemirror - Editor. * @param {Object} settings - Code editor settings. * @param {Object} settings.codeMirror - Settings for CodeMirror. * @param {Function} settings.onTabNext - Callback to handle tabbing to the next tabbable element. * @param {Function} settings.onTabPrevious - Callback to handle tabbing to the previous tabbable element. * * @return {void} */ function configureTabbing( codemirror, settings ) { var $textarea = $( codemirror.getTextArea() ); codemirror.on( 'blur', function() { $textarea.data( 'next-tab-blurs', false ); }); codemirror.on( 'keydown', function onKeydown( editor, event ) { var tabKeyCode = 9, escKeyCode = 27; // Take note of the ESC keypress so that the next TAB can focus outside the editor. if ( escKeyCode === event.keyCode ) { $textarea.data( 'next-tab-blurs', true ); return; } // Short-circuit if tab key is not being pressed or the tab key press should move focus. if ( tabKeyCode !== event.keyCode || ! $textarea.data( 'next-tab-blurs' ) ) { return; } // Focus on previous or next focusable item. if ( event.shiftKey ) { settings.onTabPrevious( codemirror, event ); } else { settings.onTabNext( codemirror, event ); } // Reset tab state. $textarea.data( 'next-tab-blurs', false ); // Prevent tab character from being added. event.preventDefault(); }); } /** * @typedef {object} wp.codeEditor~CodeEditorInstance * @property {object} settings - The code editor settings. * @property {CodeMirror} codemirror - The CodeMirror instance. */ /** * Initialize Code Editor (CodeMirror) for an existing textarea. * * @since 4.9.0 * * @param {string|jQuery|Element} textarea - The HTML id, jQuery object, or DOM Element for the textarea that is used for the editor. * @param {Object} [settings] - Settings to override defaults. * @param {Function} [settings.onChangeLintingErrors] - Callback for when the linting errors have changed. * @param {Function} [settings.onUpdateErrorNotice] - Callback for when error notice should be displayed. * @param {Function} [settings.onTabPrevious] - Callback to handle tabbing to the previous tabbable element. * @param {Function} [settings.onTabNext] - Callback to handle tabbing to the next tabbable element. * @param {Object} [settings.codemirror] - Options for CodeMirror. * @param {Object} [settings.csslint] - Rules for CSSLint. * @param {Object} [settings.htmlhint] - Rules for HTMLHint. * @param {Object} [settings.jshint] - Rules for JSHint. * * @return {CodeEditorInstance} Instance. */ wp.codeEditor.initialize = function initialize( textarea, settings ) { var $textarea, codemirror, instanceSettings, instance; if ( 'string' === typeof textarea ) { $textarea = $( '#' + textarea ); } else { $textarea = $( textarea ); } instanceSettings = $.extend( {}, wp.codeEditor.defaultSettings, settings ); instanceSettings.codemirror = $.extend( {}, instanceSettings.codemirror ); codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror ); configureLinting( codemirror, instanceSettings ); instance = { settings: instanceSettings, codemirror: codemirror }; if ( codemirror.showHint ) { codemirror.on( 'keyup', function( editor, event ) { // eslint-disable-line complexity var shouldAutocomplete, isAlphaKey = /^[a-zA-Z]$/.test( event.key ), lineBeforeCursor, innerMode, token; if ( codemirror.state.completionActive && isAlphaKey ) { return; } // Prevent autocompletion in string literals or comments. token = codemirror.getTokenAt( codemirror.getCursor() ); if ( 'string' === token.type || 'comment' === token.type ) { return; } innerMode = wp.CodeMirror.innerMode( codemirror.getMode(), token.state ).mode.name; lineBeforeCursor = codemirror.doc.getLine( codemirror.doc.getCursor().line ).substr( 0, codemirror.doc.getCursor().ch ); if ( 'html' === innerMode || 'xml' === innerMode ) { shouldAutocomplete = '<' === event.key || '/' === event.key && 'tag' === token.type || isAlphaKey && 'tag' === token.type || isAlphaKey && 'attribute' === token.type || '=' === token.string && token.state.htmlState && token.state.htmlState.tagName; } else if ( 'css' === innerMode ) { shouldAutocomplete = isAlphaKey || ':' === event.key || ' ' === event.key && /:\s+$/.test( lineBeforeCursor ); } else if ( 'javascript' === innerMode ) { shouldAutocomplete = isAlphaKey || '.' === event.key; } else if ( 'clike' === innerMode && 'php' === codemirror.options.mode ) { shouldAutocomplete = 'keyword' === token.type || 'variable' === token.type; } if ( shouldAutocomplete ) { codemirror.showHint( { completeSingle: false } ); } }); } // Facilitate tabbing out of the editor. configureTabbing( codemirror, settings ); return instance; }; })( window.jQuery, window.wp ); PK1YZE~3<<image-edit.min.jsnu[/*! This file is auto-generated */ !function(c){var s=wp.i18n.__,d=window.imageEdit={iasapi:{},hold:{},postid:"",_view:!1,toggleCropTool:function(t,i,e){var a,o,r,n=c("#image-preview-"+t),s=this.iasapi.getSelection();d.toggleControls(e),"false"==("true"===c(e).attr("aria-expanded")?"true":"false")?(this.iasapi.cancelSelection(),d.setDisabled(c(".imgedit-crop-clear"),0)):(d.setDisabled(c(".imgedit-crop-clear"),1),e=c("#imgedit-start-x-"+t).val()?c("#imgedit-start-x-"+t).val():0,a=c("#imgedit-start-y-"+t).val()?c("#imgedit-start-y-"+t).val():0,o=c("#imgedit-sel-width-"+t).val()?c("#imgedit-sel-width-"+t).val():n.innerWidth(),r=c("#imgedit-sel-height-"+t).val()?c("#imgedit-sel-height-"+t).val():n.innerHeight(),isNaN(s.x1)&&(this.setCropSelection(t,{x1:e,y1:a,x2:o,y2:r,width:o,height:r}),s=this.iasapi.getSelection()),0===s.x1&&0===s.y1&&0===s.x2&&0===s.y2?this.iasapi.setSelection(0,0,n.innerWidth(),n.innerHeight(),!0):this.iasapi.setSelection(e,a,o,r,!0),this.iasapi.setOptions({show:!0}),this.iasapi.update())},handleCropToolClick:function(t,i,e){e.classList.contains("imgedit-crop-clear")?(this.iasapi.cancelSelection(),d.setDisabled(c(".imgedit-crop-apply"),0),c("#imgedit-sel-width-"+t).val(""),c("#imgedit-sel-height-"+t).val(""),c("#imgedit-start-x-"+t).val("0"),c("#imgedit-start-y-"+t).val("0"),c("#imgedit-selection-"+t).val("")):d.crop(t,i,e)},intval:function(t){return 0|t},setDisabled:function(t,i){i?t.removeClass("disabled").prop("disabled",!1):t.addClass("disabled").prop("disabled",!0)},init:function(e){var t=this,i=c("#image-editor-"+t.postid);t.postid!==e&&i.length&&t.close(t.postid),t.hold.sizer=parseFloat(c("#imgedit-sizer-"+e).val()),t.postid=e,c("#imgedit-response-"+e).empty(),c("#imgedit-panel-"+e).on("keypress",function(t){var i=c("#imgedit-nonce-"+e).val();26===t.which&&t.ctrlKey&&d.undo(e,i),25===t.which&&t.ctrlKey&&d.redo(e,i)}),c("#imgedit-panel-"+e).on("keypress",'input[type="text"]',function(t){var i=t.keyCode;if(36this.hold.oh||r&&r>this.hold.ow?(t.css("visibility","visible"),s.prop("disabled",!0)):(t.css("visibility","hidden"),s.prop("disabled",!1)))},getSelRatio:function(t){var i=this.hold.w,e=this.hold.h,a=this.intval(c("#imgedit-crop-width-"+t).val()),t=this.intval(c("#imgedit-crop-height-"+t).val());return a&&t?a+":"+t:i&&e?i+":"+e:"1:1"},filterHistory:function(t,i){var e,a,o,r=c("#imgedit-history-"+t).val(),n=[];if(""===r)return"";if(r=JSON.parse(r),0<(e=this.intval(c("#imgedit-undone-"+t).val())))for(;0').on("load",{history:t.history},function(t){var i=c("#imgedit-crop-"+o),e=d,a=(""!==t.data.history&&(t=JSON.parse(t.data.history))[t.length-1].hasOwnProperty("c")&&(e.setDisabled(c("#image-undo-"+o),!0),c("#image-undo-"+o).trigger("focus")),i.empty().append(n),t=Math.max(e.hold.w,e.hold.h),a=Math.max(c(n).width(),c(n).height()),e.hold.sizer=a

    '+t+"

  • "),i.toggleEditor(o,0,!0),wp.a11y.speak(t,"assertive")}).attr("src",ajaxurl+"?"+c.param(t))},action:function(i,t,e){var a,o,r,n,s=this;if(s.notsaved(i))return!1;if(t={action:"image-editor",_ajax_nonce:t,postid:i},"scale"===e){if(a=c("#imgedit-scale-width-"+i),o=c("#imgedit-scale-height-"+i),r=s.intval(a.val()),n=s.intval(o.val()),r<1)return a.trigger("focus"),!1;if(n<1)return o.trigger("focus"),!1;if(r===s.hold.ow||n===s.hold.oh)return!1;t.do="scale",t.fwidth=r,t.fheight=n}else{if("restore"!==e)return!1;t.do="restore"}s.toggleEditor(i,1),c.post(ajaxurl,t,function(t){c("#image-editor-"+i).empty().append(t.data.html),s.toggleEditor(i,0,!0),s._view&&s._view.refresh()}).done(function(t){t&&t.data.message.msg?wp.a11y.speak(t.data.message.msg):t&&t.data.message.error&&wp.a11y.speak(t.data.message.error)})},save:function(i,t){var e=this.getTarget(i),a=this.filterHistory(i,0),o=this;if(""===a)return!1;this.toggleEditor(i,1),t={action:"image-editor",_ajax_nonce:t,postid:i,history:a,target:e,context:c("#image-edit-context").length?c("#image-edit-context").val():null,do:"save"},c.post(ajaxurl,t,function(t){t.data.error?(c("#imgedit-response-"+i).html('"),d.close(i),wp.a11y.speak(t.data.error)):(t.data.fw&&t.data.fh&&c("#media-dims-"+i).html(t.data.fw+" × "+t.data.fh),t.data.thumbnail&&c(".thumbnail","#thumbnail-head-"+i).attr("src",""+t.data.thumbnail),t.data.msg&&(c("#imgedit-response-"+i).html('"),wp.a11y.speak(t.data.msg)),o._view?o._view.save():d.close(i))})},open:function(e,t,i){this._view=i;var a=c("#image-editor-"+e),o=c("#media-head-"+e),r=c("#imgedit-open-btn-"+e),n=r.siblings(".spinner");if(!r.hasClass("button-activated"))return n.addClass("is-active"),c.ajax({url:ajaxurl,type:"post",data:{action:"image-editor",_ajax_nonce:t,postid:e,do:"open"},beforeSend:function(){r.addClass("button-activated")}}).done(function(t){var i;"-1"===t&&(i=s("Could not load the preview image."),a.html('")),t.data&&t.data.html&&a.html(t.data.html),o.fadeOut("fast",function(){a.fadeIn("fast",function(){i&&c(document).trigger("image-editor-ui-ready")}),r.removeClass("button-activated"),n.removeClass("is-active")}),d.init(e)})},imgLoaded:function(t){var i=c("#image-preview-"+t),e=c("#imgedit-crop-"+t);void 0===this.hold.sizer&&this.init(t),this.calculateImgSize(t),this.initCrop(t,i,e),this.setCropSelection(t,{x1:0,y1:0,x2:0,y2:0,width:i.innerWidth(),height:i.innerHeight()}),this.toggleEditor(t,0,!0)},focusManager:function(){setTimeout(function(){var t=c('.notice[role="alert"]');(t=t.length?t:c(".imgedit-wrap").find(":tabbable:first")).attr("tabindex","-1").trigger("focus")},100)},initCrop:function(r,t,i){var n=this,o=c("#imgedit-sel-width-"+r),s=c("#imgedit-sel-height-"+r),t=c(t);t.data("imgAreaSelect")||(n.iasapi=t.imgAreaSelect({parent:i,instance:!0,handles:!0,keys:!0,minWidth:3,minHeight:3,onInit:function(t){c(t).next().css("position","absolute").nextAll(".imgareaselect-outer").css("position","absolute"),i.children().on("mousedown touchstart",function(t){var i=!1,e=n.iasapi.getSelection(),a=n.intval(c("#imgedit-crop-width-"+r).val()),o=n.intval(c("#imgedit-crop-height-"+r).val());a&&o?i=n.getSelRatio(r):t.shiftKey&&e&&e.width&&e.height&&(i=e.width+":"+e.height),n.iasapi.setOptions({aspectRatio:i})})},onSelectStart:function(){d.setDisabled(c("#imgedit-crop-sel-"+r),1),d.setDisabled(c(".imgedit-crop-clear"),1),d.setDisabled(c(".imgedit-crop-apply"),1)},onSelectEnd:function(t,i){d.setCropSelection(r,i),c("#imgedit-crop > *").is(":visible")||d.toggleControls(c(".imgedit-crop.button"))},onSelectChange:function(t,i){var e=d.hold.sizer,a=d.currentCropSelection;null!=a&&a.width==i.width&&a.height==i.height||(o.val(Math.min(d.hold.w,d.round(i.width/e))),s.val(Math.min(d.hold.h,d.round(i.height/e))),n.currentCropSelection=i)}}))},setCropSelection:function(t,i){var e=c("#imgedit-sel-width-"+t),a=c("#imgedit-sel-height-"+t),o=this.hold.sizer,r=this.hold;if(!(i=i||0)||i.width<3&&i.height<3)return this.setDisabled(c(".imgedit-crop","#imgedit-panel-"+t),1),this.setDisabled(c("#imgedit-crop-sel-"+t),1),c("#imgedit-sel-width-"+t).val(""),c("#imgedit-sel-height-"+t).val(""),c("#imgedit-start-x-"+t).val("0"),c("#imgedit-start-y-"+t).val("0"),c("#imgedit-selection-"+t).val(""),!1;var n=r.w-(Math.round(i.x1/o)+parseInt(e.val())),r=r.h-(Math.round(i.y1/o)+parseInt(a.val())),n={r:1,x:Math.round(i.x1/o)+Math.min(0,n),y:Math.round(i.y1/o)+Math.min(0,r),w:e.val(),h:a.val()};this.setDisabled(c(".imgedit-crop","#imgedit-panel-"+t),1),c("#imgedit-selection-"+t).val(JSON.stringify(n))},close:function(t,i){if((i=i||!1)&&this.notsaved(t))return!1;this.iasapi={},this.hold={},this._view?this._view.back():c("#image-editor-"+t).fadeOut("fast",function(){c("#media-head-"+t).fadeIn("fast",function(){c("#imgedit-open-btn-"+t).trigger("focus")}),c(this).empty()})},notsaved:function(t){var i=c("#imgedit-history-"+t).val(),i=""!==i?JSON.parse(i):[];return this.intval(c("#imgedit-undone-"+t).val())

    '+o+"

    "),wp.a11y.speak(o,"assertive"),c(i?"#imgedit-crop-height-"+t:"#imgedit-crop-width-"+t).val("")):void 0!==(r=c("#imgedit-crop-"+t).find(".notice-error"))&&r.remove(),this.iasapi.setSelection(e.x1,e.y1,e.x2,a),this.iasapi.update())},validateNumeric:function(t){if(!1===this.intval(c(t).val()))return c(t).val(""),!1}}}(jQuery);PK1YZaM image-edit.jsnu[/** * The functions necessary for editing images. * * @since 2.9.0 * @output wp-admin/js/image-edit.js */ /* global ajaxurl, confirm */ (function($) { var __ = wp.i18n.__; /** * Contains all the methods to initialize and control the image editor. * * @namespace imageEdit */ var imageEdit = window.imageEdit = { iasapi : {}, hold : {}, postid : '', _view : false, /** * Enable crop tool. */ toggleCropTool: function( postid, nonce, cropButton ) { var img = $( '#image-preview-' + postid ), selection = this.iasapi.getSelection(); imageEdit.toggleControls( cropButton ); var $el = $( cropButton ); var state = ( $el.attr( 'aria-expanded' ) === 'true' ) ? 'true' : 'false'; // Crop tools have been closed. if ( 'false' === state ) { // Cancel selection, but do not unset inputs. this.iasapi.cancelSelection(); imageEdit.setDisabled($('.imgedit-crop-clear'), 0); } else { imageEdit.setDisabled($('.imgedit-crop-clear'), 1); // Get values from inputs to restore previous selection. var startX = ( $( '#imgedit-start-x-' + postid ).val() ) ? $('#imgedit-start-x-' + postid).val() : 0; var startY = ( $( '#imgedit-start-y-' + postid ).val() ) ? $('#imgedit-start-y-' + postid).val() : 0; var width = ( $( '#imgedit-sel-width-' + postid ).val() ) ? $('#imgedit-sel-width-' + postid).val() : img.innerWidth(); var height = ( $( '#imgedit-sel-height-' + postid ).val() ) ? $('#imgedit-sel-height-' + postid).val() : img.innerHeight(); // Ensure selection is available, otherwise reset to full image. if ( isNaN( selection.x1 ) ) { this.setCropSelection( postid, { 'x1': startX, 'y1': startY, 'x2': width, 'y2': height, 'width': width, 'height': height } ); selection = this.iasapi.getSelection(); } // If we don't already have a selection, select the entire image. if ( 0 === selection.x1 && 0 === selection.y1 && 0 === selection.x2 && 0 === selection.y2 ) { this.iasapi.setSelection( 0, 0, img.innerWidth(), img.innerHeight(), true ); this.iasapi.setOptions( { show: true } ); this.iasapi.update(); } else { this.iasapi.setSelection( startX, startY, width, height, true ); this.iasapi.setOptions( { show: true } ); this.iasapi.update(); } } }, /** * Handle crop tool clicks. */ handleCropToolClick: function( postid, nonce, cropButton ) { if ( cropButton.classList.contains( 'imgedit-crop-clear' ) ) { this.iasapi.cancelSelection(); imageEdit.setDisabled($('.imgedit-crop-apply'), 0); $('#imgedit-sel-width-' + postid).val(''); $('#imgedit-sel-height-' + postid).val(''); $('#imgedit-start-x-' + postid).val('0'); $('#imgedit-start-y-' + postid).val('0'); $('#imgedit-selection-' + postid).val(''); } else { // Otherwise, perform the crop. imageEdit.crop( postid, nonce , cropButton ); } }, /** * Converts a value to an integer. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} f The float value that should be converted. * * @return {number} The integer representation from the float value. */ intval : function(f) { /* * Bitwise OR operator: one of the obscure ways to truncate floating point figures, * worth reminding JavaScript doesn't have a distinct "integer" type. */ return f | 0; }, /** * Adds the disabled attribute and class to a single form element or a field set. * * @since 2.9.0 * * @memberof imageEdit * * @param {jQuery} el The element that should be modified. * @param {boolean|number} s The state for the element. If set to true * the element is disabled, * otherwise the element is enabled. * The function is sometimes called with a 0 or 1 * instead of true or false. * * @return {void} */ setDisabled : function( el, s ) { /* * `el` can be a single form element or a fieldset. Before #28864, the disabled state on * some text fields was handled targeting $('input', el). Now we need to handle the * disabled state on buttons too so we can just target `el` regardless if it's a single * element or a fieldset because when a fieldset is disabled, its descendants are disabled too. */ if ( s ) { el.removeClass( 'disabled' ).prop( 'disabled', false ); } else { el.addClass( 'disabled' ).prop( 'disabled', true ); } }, /** * Initializes the image editor. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * * @return {void} */ init : function(postid) { var t = this, old = $('#image-editor-' + t.postid); if ( t.postid !== postid && old.length ) { t.close(t.postid); } t.hold.sizer = parseFloat( $('#imgedit-sizer-' + postid).val() ); t.postid = postid; $('#imgedit-response-' + postid).empty(); $('#imgedit-panel-' + postid).on( 'keypress', function(e) { var nonce = $( '#imgedit-nonce-' + postid ).val(); if ( e.which === 26 && e.ctrlKey ) { imageEdit.undo( postid, nonce ); } if ( e.which === 25 && e.ctrlKey ) { imageEdit.redo( postid, nonce ); } }); $('#imgedit-panel-' + postid).on( 'keypress', 'input[type="text"]', function(e) { var k = e.keyCode; // Key codes 37 through 40 are the arrow keys. if ( 36 < k && k < 41 ) { $(this).trigger( 'blur' ); } // The key code 13 is the Enter key. if ( 13 === k ) { e.preventDefault(); e.stopPropagation(); return false; } }); $( document ).on( 'image-editor-ui-ready', this.focusManager ); }, /** * Calculate the image size and save it to memory. * * @since 6.7.0 * * @memberof imageEdit * * @param {number} postid The post ID. * * @return {void} */ calculateImgSize: function( postid ) { var t = this, x = t.intval( $( '#imgedit-x-' + postid ).val() ), y = t.intval( $( '#imgedit-y-' + postid ).val() ); t.hold.w = t.hold.ow = x; t.hold.h = t.hold.oh = y; t.hold.xy_ratio = x / y; t.hold.sizer = parseFloat( $( '#imgedit-sizer-' + postid ).val() ); t.currentCropSelection = null; }, /** * Toggles the wait/load icon in the editor. * * @since 2.9.0 * @since 5.5.0 Added the triggerUIReady parameter. * * @memberof imageEdit * * @param {number} postid The post ID. * @param {number} toggle Is 0 or 1, fades the icon in when 1 and out when 0. * @param {boolean} triggerUIReady Whether to trigger a custom event when the UI is ready. Default false. * * @return {void} */ toggleEditor: function( postid, toggle, triggerUIReady ) { var wait = $('#imgedit-wait-' + postid); if ( toggle ) { wait.fadeIn( 'fast' ); } else { wait.fadeOut( 'fast', function() { if ( triggerUIReady ) { $( document ).trigger( 'image-editor-ui-ready' ); } } ); } }, /** * Shows or hides image menu popup. * * @since 6.3.0 * * @memberof imageEdit * * @param {HTMLElement} el The activated control element. * * @return {boolean} Always returns false. */ togglePopup : function(el) { var $el = $( el ); var $targetEl = $( el ).attr( 'aria-controls' ); var $target = $( '#' + $targetEl ); $el .attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' ); // Open menu and set z-index to appear above image crop area if it is enabled. $target .toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' ).css( { 'z-index' : 200000 } ); // Move focus to first item in menu when opening menu. if ( 'true' === $el.attr( 'aria-expanded' ) ) { $target.find( 'button' ).first().trigger( 'focus' ); } return false; }, /** * Observes whether the popup should remain open based on focus position. * * @since 6.4.0 * * @memberof imageEdit * * @param {HTMLElement} el The activated control element. * * @return {boolean} Always returns false. */ monitorPopup : function() { var $parent = document.querySelector( '.imgedit-rotate-menu-container' ); var $toggle = document.querySelector( '.imgedit-rotate-menu-container .imgedit-rotate' ); setTimeout( function() { var $focused = document.activeElement; var $contains = $parent.contains( $focused ); // If $focused is defined and not inside the menu container, close the popup. if ( $focused && ! $contains ) { if ( 'true' === $toggle.getAttribute( 'aria-expanded' ) ) { imageEdit.togglePopup( $toggle ); } } }, 100 ); return false; }, /** * Navigate popup menu by arrow keys. * * @since 6.3.0 * @since 6.7.0 Added the event parameter. * * @memberof imageEdit * * @param {Event} event The key or click event. * @param {HTMLElement} el The current element. * * @return {boolean} Always returns false. */ browsePopup : function(event, el) { var $el = $( el ); var $collection = $( el ).parent( '.imgedit-popup-menu' ).find( 'button' ); var $index = $collection.index( $el ); var $prev = $index - 1; var $next = $index + 1; var $last = $collection.length; if ( $prev < 0 ) { $prev = $last - 1; } if ( $next === $last ) { $next = 0; } var target = false; if ( event.keyCode === 40 ) { target = $collection.get( $next ); } else if ( event.keyCode === 38 ) { target = $collection.get( $prev ); } if ( target ) { target.focus(); event.preventDefault(); } return false; }, /** * Close popup menu and reset focus on feature activation. * * @since 6.3.0 * * @memberof imageEdit * * @param {HTMLElement} el The current element. * * @return {boolean} Always returns false. */ closePopup : function(el) { var $parent = $(el).parent( '.imgedit-popup-menu' ); var $controlledID = $parent.attr( 'id' ); var $target = $( 'button[aria-controls="' + $controlledID + '"]' ); $target .attr( 'aria-expanded', 'false' ).trigger( 'focus' ); $parent .toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' ); return false; }, /** * Shows or hides the image edit help box. * * @since 2.9.0 * * @memberof imageEdit * * @param {HTMLElement} el The element to create the help window in. * * @return {boolean} Always returns false. */ toggleHelp : function(el) { var $el = $( el ); $el .attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' ) .parents( '.imgedit-group-top' ).toggleClass( 'imgedit-help-toggled' ).find( '.imgedit-help' ).slideToggle( 'fast' ); return false; }, /** * Shows or hides image edit input fields when enabled. * * @since 6.3.0 * * @memberof imageEdit * * @param {HTMLElement} el The element to trigger the edit panel. * * @return {boolean} Always returns false. */ toggleControls : function(el) { var $el = $( el ); var $target = $( '#' + $el.attr( 'aria-controls' ) ); $el .attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' ); $target .parent( '.imgedit-group' ).toggleClass( 'imgedit-panel-active' ); return false; }, /** * Gets the value from the image edit target. * * The image edit target contains the image sizes where the (possible) changes * have to be applied to. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * * @return {string} The value from the imagedit-save-target input field when available, * 'full' when not selected, or 'all' if it doesn't exist. */ getTarget : function( postid ) { var element = $( '#imgedit-save-target-' + postid ); if ( element.length ) { return element.find( 'input[name="imgedit-target-' + postid + '"]:checked' ).val() || 'full'; } return 'all'; }, /** * Recalculates the height or width and keeps the original aspect ratio. * * If the original image size is exceeded a red exclamation mark is shown. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The current post ID. * @param {number} x Is 0 when it applies the y-axis * and 1 when applicable for the x-axis. * @param {jQuery} el Element. * * @return {void} */ scaleChanged : function( postid, x, el ) { var w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid), warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = '', scaleBtn = $('#imgedit-scale-button'); if ( false === this.validateNumeric( el ) ) { return; } if ( x ) { h1 = ( w.val() !== '' ) ? Math.round( w.val() / this.hold.xy_ratio ) : ''; h.val( h1 ); } else { w1 = ( h.val() !== '' ) ? Math.round( h.val() * this.hold.xy_ratio ) : ''; w.val( w1 ); } if ( ( h1 && h1 > this.hold.oh ) || ( w1 && w1 > this.hold.ow ) ) { warn.css('visibility', 'visible'); scaleBtn.prop('disabled', true); } else { warn.css('visibility', 'hidden'); scaleBtn.prop('disabled', false); } }, /** * Gets the selected aspect ratio. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * * @return {string} The aspect ratio. */ getSelRatio : function(postid) { var x = this.hold.w, y = this.hold.h, X = this.intval( $('#imgedit-crop-width-' + postid).val() ), Y = this.intval( $('#imgedit-crop-height-' + postid).val() ); if ( X && Y ) { return X + ':' + Y; } if ( x && y ) { return x + ':' + y; } return '1:1'; }, /** * Removes the last action from the image edit history. * The history consist of (edit) actions performed on the image. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {number} setSize 0 or 1, when 1 the image resets to its original size. * * @return {string} JSON string containing the history or an empty string if no history exists. */ filterHistory : function(postid, setSize) { // Apply undo state to history. var history = $('#imgedit-history-' + postid).val(), pop, n, o, i, op = []; if ( history !== '' ) { // Read the JSON string with the image edit history. history = JSON.parse(history); pop = this.intval( $('#imgedit-undone-' + postid).val() ); if ( pop > 0 ) { while ( pop > 0 ) { history.pop(); pop--; } } // Reset size to its original state. if ( setSize ) { if ( !history.length ) { this.hold.w = this.hold.ow; this.hold.h = this.hold.oh; return ''; } // Restore original 'o'. o = history[history.length - 1]; // c = 'crop', r = 'rotate', f = 'flip'. o = o.c || o.r || o.f || false; if ( o ) { // fw = Full image width. this.hold.w = o.fw; // fh = Full image height. this.hold.h = o.fh; } } // Filter the last step/action from the history. for ( n in history ) { i = history[n]; if ( i.hasOwnProperty('c') ) { op[n] = { 'c': { 'x': i.c.x, 'y': i.c.y, 'w': i.c.w, 'h': i.c.h, 'r': i.c.r } }; } else if ( i.hasOwnProperty('r') ) { op[n] = { 'r': i.r.r }; } else if ( i.hasOwnProperty('f') ) { op[n] = { 'f': i.f.f }; } } return JSON.stringify(op); } return ''; }, /** * Binds the necessary events to the image. * * When the image source is reloaded the image will be reloaded. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {string} nonce The nonce to verify the request. * @param {function} callback Function to execute when the image is loaded. * * @return {void} */ refreshEditor : function(postid, nonce, callback) { var t = this, data, img; t.toggleEditor(postid, 1); data = { 'action': 'imgedit-preview', '_ajax_nonce': nonce, 'postid': postid, 'history': t.filterHistory(postid, 1), 'rand': t.intval(Math.random() * 1000000) }; img = $( '' ) .on( 'load', { history: data.history }, function( event ) { var max1, max2, parent = $( '#imgedit-crop-' + postid ), t = imageEdit, historyObj; // Checks if there already is some image-edit history. if ( '' !== event.data.history ) { historyObj = JSON.parse( event.data.history ); // If last executed action in history is a crop action. if ( historyObj[historyObj.length - 1].hasOwnProperty( 'c' ) ) { /* * A crop action has completed and the crop button gets disabled * ensure the undo button is enabled. */ t.setDisabled( $( '#image-undo-' + postid) , true ); // Move focus to the undo button to avoid a focus loss. $( '#image-undo-' + postid ).trigger( 'focus' ); } } parent.empty().append(img); // w, h are the new full size dimensions. max1 = Math.max( t.hold.w, t.hold.h ); max2 = Math.max( $(img).width(), $(img).height() ); t.hold.sizer = max1 > max2 ? max2 / max1 : 1; t.initCrop(postid, img, parent); if ( (typeof callback !== 'undefined') && callback !== null ) { callback(); } if ( $('#imgedit-history-' + postid).val() && $('#imgedit-undone-' + postid).val() === '0' ) { $('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false); } else { $('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true); } var successMessage = __( 'Image updated.' ); t.toggleEditor(postid, 0); wp.a11y.speak( successMessage, 'assertive' ); }) .on( 'error', function() { var errorMessage = __( 'Could not load the preview image. Please reload the page and try again.' ); $( '#imgedit-crop-' + postid ) .empty() .append( '' ); t.toggleEditor( postid, 0, true ); wp.a11y.speak( errorMessage, 'assertive' ); } ) .attr('src', ajaxurl + '?' + $.param(data)); }, /** * Performs an image edit action. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {string} nonce The nonce to verify the request. * @param {string} action The action to perform on the image. * The possible actions are: "scale" and "restore". * * @return {boolean|void} Executes a post request that refreshes the page * when the action is performed. * Returns false if an invalid action is given, * or when the action cannot be performed. */ action : function(postid, nonce, action) { var t = this, data, w, h, fw, fh; if ( t.notsaved(postid) ) { return false; } data = { 'action': 'image-editor', '_ajax_nonce': nonce, 'postid': postid }; if ( 'scale' === action ) { w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid), fw = t.intval(w.val()), fh = t.intval(h.val()); if ( fw < 1 ) { w.trigger( 'focus' ); return false; } else if ( fh < 1 ) { h.trigger( 'focus' ); return false; } if ( fw === t.hold.ow || fh === t.hold.oh ) { return false; } data['do'] = 'scale'; data.fwidth = fw; data.fheight = fh; } else if ( 'restore' === action ) { data['do'] = 'restore'; } else { return false; } t.toggleEditor(postid, 1); $.post( ajaxurl, data, function( response ) { $( '#image-editor-' + postid ).empty().append( response.data.html ); t.toggleEditor( postid, 0, true ); // Refresh the attachment model so that changes propagate. if ( t._view ) { t._view.refresh(); } } ).done( function( response ) { // Whether the executed action was `scale` or `restore`, the response does have a message. if ( response && response.data.message.msg ) { wp.a11y.speak( response.data.message.msg ); return; } if ( response && response.data.message.error ) { wp.a11y.speak( response.data.message.error ); } } ); }, /** * Stores the changes that are made to the image. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID to get the image from the database. * @param {string} nonce The nonce to verify the request. * * @return {boolean|void} If the actions are successfully saved a response message is shown. * Returns false if there is no image editing history, * thus there are not edit-actions performed on the image. */ save : function(postid, nonce) { var data, target = this.getTarget(postid), history = this.filterHistory(postid, 0), self = this; if ( '' === history ) { return false; } this.toggleEditor(postid, 1); data = { 'action': 'image-editor', '_ajax_nonce': nonce, 'postid': postid, 'history': history, 'target': target, 'context': $('#image-edit-context').length ? $('#image-edit-context').val() : null, 'do': 'save' }; // Post the image edit data to the backend. $.post( ajaxurl, data, function( response ) { // If a response is returned, close the editor and show an error. if ( response.data.error ) { $( '#imgedit-response-' + postid ) .html( '' ); imageEdit.close(postid); wp.a11y.speak( response.data.error ); return; } if ( response.data.fw && response.data.fh ) { $( '#media-dims-' + postid ).html( response.data.fw + ' × ' + response.data.fh ); } if ( response.data.thumbnail ) { $( '.thumbnail', '#thumbnail-head-' + postid ).attr( 'src', '' + response.data.thumbnail ); } if ( response.data.msg ) { $( '#imgedit-response-' + postid ) .html( '' ); wp.a11y.speak( response.data.msg ); } if ( self._view ) { self._view.save(); } else { imageEdit.close(postid); } }); }, /** * Creates the image edit window. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID for the image. * @param {string} nonce The nonce to verify the request. * @param {Object} view The image editor view to be used for the editing. * * @return {void|promise} Either returns void if the button was already activated * or returns an instance of the image editor, wrapped in a promise. */ open : function( postid, nonce, view ) { this._view = view; var dfd, data, elem = $( '#image-editor-' + postid ), head = $( '#media-head-' + postid ), btn = $( '#imgedit-open-btn-' + postid ), spin = btn.siblings( '.spinner' ); /* * Instead of disabling the button, which causes a focus loss and makes screen * readers announce "unavailable", return if the button was already clicked. */ if ( btn.hasClass( 'button-activated' ) ) { return; } spin.addClass( 'is-active' ); data = { 'action': 'image-editor', '_ajax_nonce': nonce, 'postid': postid, 'do': 'open' }; dfd = $.ajax( { url: ajaxurl, type: 'post', data: data, beforeSend: function() { btn.addClass( 'button-activated' ); } } ).done( function( response ) { var errorMessage; if ( '-1' === response ) { errorMessage = __( 'Could not load the preview image.' ); elem.html( '' ); } if ( response.data && response.data.html ) { elem.html( response.data.html ); } head.fadeOut( 'fast', function() { elem.fadeIn( 'fast', function() { if ( errorMessage ) { $( document ).trigger( 'image-editor-ui-ready' ); } } ); btn.removeClass( 'button-activated' ); spin.removeClass( 'is-active' ); } ); // Initialize the Image Editor now that everything is ready. imageEdit.init( postid ); } ); return dfd; }, /** * Initializes the cropping tool and sets a default cropping selection. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * * @return {void} */ imgLoaded : function(postid) { var img = $('#image-preview-' + postid), parent = $('#imgedit-crop-' + postid); // Ensure init has run even when directly loaded. if ( 'undefined' === typeof this.hold.sizer ) { this.init( postid ); } this.calculateImgSize( postid ); this.initCrop(postid, img, parent); this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0, 'width': img.innerWidth(), 'height': img.innerHeight() } ); this.toggleEditor( postid, 0, true ); }, /** * Manages keyboard focus in the Image Editor user interface. * * @since 5.5.0 * * @return {void} */ focusManager: function() { /* * Editor is ready. Move focus to one of the admin alert notices displayed * after a user action or to the first focusable element. Since the DOM * update is pretty large, the timeout helps browsers update their * accessibility tree to better support assistive technologies. */ setTimeout( function() { var elementToSetFocusTo = $( '.notice[role="alert"]' ); if ( ! elementToSetFocusTo.length ) { elementToSetFocusTo = $( '.imgedit-wrap' ).find( ':tabbable:first' ); } elementToSetFocusTo.attr( 'tabindex', '-1' ).trigger( 'focus' ); }, 100 ); }, /** * Initializes the cropping tool. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {HTMLElement} image The preview image. * @param {HTMLElement} parent The preview image container. * * @return {void} */ initCrop : function(postid, image, parent) { var t = this, selW = $('#imgedit-sel-width-' + postid), selH = $('#imgedit-sel-height-' + postid), $image = $( image ), $img; // Already initialized? if ( $image.data( 'imgAreaSelect' ) ) { return; } t.iasapi = $image.imgAreaSelect({ parent: parent, instance: true, handles: true, keys: true, minWidth: 3, minHeight: 3, /** * Sets the CSS styles and binds events for locking the aspect ratio. * * @ignore * * @param {jQuery} img The preview image. */ onInit: function( img ) { // Ensure that the imgAreaSelect wrapper elements are position:absolute // (even if we're in a position:fixed modal). $img = $( img ); $img.next().css( 'position', 'absolute' ) .nextAll( '.imgareaselect-outer' ).css( 'position', 'absolute' ); /** * Binds mouse down event to the cropping container. * * @return {void} */ parent.children().on( 'mousedown touchstart', function(e) { var ratio = false, sel = t.iasapi.getSelection(), cx = t.intval( $( '#imgedit-crop-width-' + postid ).val() ), cy = t.intval( $( '#imgedit-crop-height-' + postid ).val() ); if ( cx && cy ) { ratio = t.getSelRatio( postid ); } else if ( e.shiftKey && sel && sel.width && sel.height ) { ratio = sel.width + ':' + sel.height; } t.iasapi.setOptions({ aspectRatio: ratio }); }); }, /** * Event triggered when starting a selection. * * @ignore * * @return {void} */ onSelectStart: function() { imageEdit.setDisabled($('#imgedit-crop-sel-' + postid), 1); imageEdit.setDisabled($('.imgedit-crop-clear'), 1); imageEdit.setDisabled($('.imgedit-crop-apply'), 1); }, /** * Event triggered when the selection is ended. * * @ignore * * @param {Object} img jQuery object representing the image. * @param {Object} c The selection. * * @return {Object} */ onSelectEnd: function(img, c) { imageEdit.setCropSelection(postid, c); if ( ! $('#imgedit-crop > *').is(':visible') ) { imageEdit.toggleControls($('.imgedit-crop.button')); } }, /** * Event triggered when the selection changes. * * @ignore * * @param {Object} img jQuery object representing the image. * @param {Object} c The selection. * * @return {void} */ onSelectChange: function(img, c) { var sizer = imageEdit.hold.sizer, oldSel = imageEdit.currentCropSelection; if ( oldSel != null && oldSel.width == c.width && oldSel.height == c.height ) { return; } selW.val( Math.min( imageEdit.hold.w, imageEdit.round( c.width / sizer ) ) ); selH.val( Math.min( imageEdit.hold.h, imageEdit.round( c.height / sizer ) ) ); t.currentCropSelection = c; } }); }, /** * Stores the current crop selection. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {Object} c The selection. * * @return {boolean} */ setCropSelection : function(postid, c) { var sel, selW = $( '#imgedit-sel-width-' + postid ), selH = $( '#imgedit-sel-height-' + postid ), sizer = this.hold.sizer, hold = this.hold; c = c || 0; if ( !c || ( c.width < 3 && c.height < 3 ) ) { this.setDisabled( $( '.imgedit-crop', '#imgedit-panel-' + postid ), 1 ); this.setDisabled( $( '#imgedit-crop-sel-' + postid ), 1 ); $('#imgedit-sel-width-' + postid).val(''); $('#imgedit-sel-height-' + postid).val(''); $('#imgedit-start-x-' + postid).val('0'); $('#imgedit-start-y-' + postid).val('0'); $('#imgedit-selection-' + postid).val(''); return false; } // adjust the selection within the bounds of the image on 100% scale var excessW = hold.w - ( Math.round( c.x1 / sizer ) + parseInt( selW.val() ) ); var excessH = hold.h - ( Math.round( c.y1 / sizer ) + parseInt( selH.val() ) ); var x = Math.round( c.x1 / sizer ) + Math.min( 0, excessW ); var y = Math.round( c.y1 / sizer ) + Math.min( 0, excessH ); // use 100% scaling to prevent rounding errors sel = { 'r': 1, 'x': x, 'y': y, 'w': selW.val(), 'h': selH.val() }; this.setDisabled($('.imgedit-crop', '#imgedit-panel-' + postid), 1); $('#imgedit-selection-' + postid).val( JSON.stringify(sel) ); }, /** * Closes the image editor. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {boolean} warn Warning message. * * @return {void|boolean} Returns false if there is a warning. */ close : function(postid, warn) { warn = warn || false; if ( warn && this.notsaved(postid) ) { return false; } this.iasapi = {}; this.hold = {}; // If we've loaded the editor in the context of a Media Modal, // then switch to the previous view, whatever that might have been. if ( this._view ){ this._view.back(); } // In case we are not accessing the image editor in the context of a View, // close the editor the old-school way. else { $('#image-editor-' + postid).fadeOut('fast', function() { $( '#media-head-' + postid ).fadeIn( 'fast', function() { // Move focus back to the Edit Image button. Runs also when saving. $( '#imgedit-open-btn-' + postid ).trigger( 'focus' ); }); $(this).empty(); }); } }, /** * Checks if the image edit history is saved. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * * @return {boolean} Returns true if the history is not saved. */ notsaved : function(postid) { var h = $('#imgedit-history-' + postid).val(), history = ( h !== '' ) ? JSON.parse(h) : [], pop = this.intval( $('#imgedit-undone-' + postid).val() ); if ( pop < history.length ) { if ( confirm( $('#imgedit-leaving-' + postid).text() ) ) { return false; } return true; } return false; }, /** * Adds an image edit action to the history. * * @since 2.9.0 * * @memberof imageEdit * * @param {Object} op The original position. * @param {number} postid The post ID. * @param {string} nonce The nonce. * * @return {void} */ addStep : function(op, postid, nonce) { var t = this, elem = $('#imgedit-history-' + postid), history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : [], undone = $( '#imgedit-undone-' + postid ), pop = t.intval( undone.val() ); while ( pop > 0 ) { history.pop(); pop--; } undone.val(0); // Reset. history.push(op); elem.val( JSON.stringify(history) ); t.refreshEditor(postid, nonce, function() { t.setDisabled($('#image-undo-' + postid), true); t.setDisabled($('#image-redo-' + postid), false); }); }, /** * Rotates the image. * * @since 2.9.0 * * @memberof imageEdit * * @param {string} angle The angle the image is rotated with. * @param {number} postid The post ID. * @param {string} nonce The nonce. * @param {Object} t The target element. * * @return {boolean} */ rotate : function(angle, postid, nonce, t) { if ( $(t).hasClass('disabled') ) { return false; } this.closePopup(t); this.addStep({ 'r': { 'r': angle, 'fw': this.hold.h, 'fh': this.hold.w }}, postid, nonce); // Clear the selection fields after rotating. $( '#imgedit-sel-width-' + postid ).val( '' ); $( '#imgedit-sel-height-' + postid ).val( '' ); this.currentCropSelection = null; }, /** * Flips the image. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} axis The axle the image is flipped on. * @param {number} postid The post ID. * @param {string} nonce The nonce. * @param {Object} t The target element. * * @return {boolean} */ flip : function (axis, postid, nonce, t) { if ( $(t).hasClass('disabled') ) { return false; } this.closePopup(t); this.addStep({ 'f': { 'f': axis, 'fw': this.hold.w, 'fh': this.hold.h }}, postid, nonce); // Clear the selection fields after flipping. $( '#imgedit-sel-width-' + postid ).val( '' ); $( '#imgedit-sel-height-' + postid ).val( '' ); this.currentCropSelection = null; }, /** * Crops the image. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {string} nonce The nonce. * @param {Object} t The target object. * * @return {void|boolean} Returns false if the crop button is disabled. */ crop : function (postid, nonce, t) { var sel = $('#imgedit-selection-' + postid).val(), w = this.intval( $('#imgedit-sel-width-' + postid).val() ), h = this.intval( $('#imgedit-sel-height-' + postid).val() ); if ( $(t).hasClass('disabled') || sel === '' ) { return false; } sel = JSON.parse(sel); if ( sel.w > 0 && sel.h > 0 && w > 0 && h > 0 ) { sel.fw = w; sel.fh = h; this.addStep({ 'c': sel }, postid, nonce); } // Clear the selection fields after cropping. $( '#imgedit-sel-width-' + postid ).val( '' ); $( '#imgedit-sel-height-' + postid ).val( '' ); $( '#imgedit-start-x-' + postid ).val( '0' ); $( '#imgedit-start-y-' + postid ).val( '0' ); this.currentCropSelection = null; }, /** * Undoes an image edit action. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {string} nonce The nonce. * * @return {void|false} Returns false if the undo button is disabled. */ undo : function (postid, nonce) { var t = this, button = $('#image-undo-' + postid), elem = $('#imgedit-undone-' + postid), pop = t.intval( elem.val() ) + 1; if ( button.hasClass('disabled') ) { return; } elem.val(pop); t.refreshEditor(postid, nonce, function() { var elem = $('#imgedit-history-' + postid), history = ( elem.val() !== '' ) ? JSON.parse( elem.val() ) : []; t.setDisabled($('#image-redo-' + postid), true); t.setDisabled(button, pop < history.length); // When undo gets disabled, move focus to the redo button to avoid a focus loss. if ( history.length === pop ) { $( '#image-redo-' + postid ).trigger( 'focus' ); } }); }, /** * Reverts a undo action. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {string} nonce The nonce. * * @return {void} */ redo : function(postid, nonce) { var t = this, button = $('#image-redo-' + postid), elem = $('#imgedit-undone-' + postid), pop = t.intval( elem.val() ) - 1; if ( button.hasClass('disabled') ) { return; } elem.val(pop); t.refreshEditor(postid, nonce, function() { t.setDisabled($('#image-undo-' + postid), true); t.setDisabled(button, pop > 0); // When redo gets disabled, move focus to the undo button to avoid a focus loss. if ( 0 === pop ) { $( '#image-undo-' + postid ).trigger( 'focus' ); } }); }, /** * Sets the selection for the height and width in pixels. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {jQuery} el The element containing the values. * * @return {void|boolean} Returns false when the x or y value is lower than 1, * void when the value is not numeric or when the operation * is successful. */ setNumSelection : function( postid, el ) { var sel, elX = $('#imgedit-sel-width-' + postid), elY = $('#imgedit-sel-height-' + postid), elX1 = $('#imgedit-start-x-' + postid), elY1 = $('#imgedit-start-y-' + postid), xS = this.intval( elX1.val() ), yS = this.intval( elY1.val() ), x = this.intval( elX.val() ), y = this.intval( elY.val() ), img = $('#image-preview-' + postid), imgh = img.height(), imgw = img.width(), sizer = this.hold.sizer, x1, y1, x2, y2, ias = this.iasapi; this.currentCropSelection = null; if ( false === this.validateNumeric( el ) ) { return; } if ( x < 1 ) { elX.val(''); return false; } if ( y < 1 ) { elY.val(''); return false; } if ( ( ( x && y ) || ( xS && yS ) ) && ( sel = ias.getSelection() ) ) { x2 = sel.x1 + Math.round( x * sizer ); y2 = sel.y1 + Math.round( y * sizer ); x1 = ( xS === sel.x1 ) ? sel.x1 : Math.round( xS * sizer ); y1 = ( yS === sel.y1 ) ? sel.y1 : Math.round( yS * sizer ); if ( x2 > imgw ) { x1 = 0; x2 = imgw; elX.val( Math.min( this.hold.w, Math.round( x2 / sizer ) ) ); } if ( y2 > imgh ) { y1 = 0; y2 = imgh; elY.val( Math.min( this.hold.h, Math.round( y2 / sizer ) ) ); } ias.setSelection( x1, y1, x2, y2 ); ias.update(); this.setCropSelection(postid, ias.getSelection()); this.currentCropSelection = ias.getSelection(); } }, /** * Rounds a number to a whole. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} num The number. * * @return {number} The number rounded to a whole number. */ round : function(num) { var s; num = Math.round(num); if ( this.hold.sizer > 0.6 ) { return num; } s = num.toString().slice(-1); if ( '1' === s ) { return num - 1; } else if ( '9' === s ) { return num + 1; } return num; }, /** * Sets a locked aspect ratio for the selection. * * @since 2.9.0 * * @memberof imageEdit * * @param {number} postid The post ID. * @param {number} n The ratio to set. * @param {jQuery} el The element containing the values. * * @return {void} */ setRatioSelection : function(postid, n, el) { var sel, r, x = this.intval( $('#imgedit-crop-width-' + postid).val() ), y = this.intval( $('#imgedit-crop-height-' + postid).val() ), h = $('#image-preview-' + postid).height(); if ( false === this.validateNumeric( el ) ) { this.iasapi.setOptions({ aspectRatio: null }); return; } if ( x && y ) { this.iasapi.setOptions({ aspectRatio: x + ':' + y }); if ( sel = this.iasapi.getSelection(true) ) { r = Math.ceil( sel.y1 + ( ( sel.x2 - sel.x1 ) / ( x / y ) ) ); if ( r > h ) { r = h; var errorMessage = __( 'Selected crop ratio exceeds the boundaries of the image. Try a different ratio.' ); $( '#imgedit-crop-' + postid ) .prepend( '' ); wp.a11y.speak( errorMessage, 'assertive' ); if ( n ) { $('#imgedit-crop-height-' + postid).val( '' ); } else { $('#imgedit-crop-width-' + postid).val( ''); } } else { var error = $( '#imgedit-crop-' + postid ).find( '.notice-error' ); if ( 'undefined' !== typeof( error ) ) { error.remove(); } } this.iasapi.setSelection( sel.x1, sel.y1, sel.x2, r ); this.iasapi.update(); } } }, /** * Validates if a value in a jQuery.HTMLElement is numeric. * * @since 4.6.0 * * @memberof imageEdit * * @param {jQuery} el The html element. * * @return {void|boolean} Returns false if the value is not numeric, * void when it is. */ validateNumeric: function( el ) { if ( false === this.intval( $( el ).val() ) ) { $( el ).val( '' ); return false; } } }; })(jQuery); PK1YZ^ɶɶcustomize-nav-menus.jsnu[/** * @output wp-admin/js/customize-nav-menus.js */ /* global menus, _wpCustomizeNavMenusSettings, wpNavMenu, console */ ( function( api, wp, $ ) { 'use strict'; /** * Set up wpNavMenu for drag and drop. */ wpNavMenu.originalInit = wpNavMenu.init; wpNavMenu.options.menuItemDepthPerLevel = 20; wpNavMenu.options.sortableItems = '> .customize-control-nav_menu_item'; wpNavMenu.options.targetTolerance = 10; wpNavMenu.init = function() { this.jQueryExtensions(); }; /** * @namespace wp.customize.Menus */ api.Menus = api.Menus || {}; // Link settings. api.Menus.data = { itemTypes: [], l10n: {}, settingTransport: 'refresh', phpIntMax: 0, defaultSettingValues: { nav_menu: {}, nav_menu_item: {} }, locationSlugMappedToName: {} }; if ( 'undefined' !== typeof _wpCustomizeNavMenusSettings ) { $.extend( api.Menus.data, _wpCustomizeNavMenusSettings ); } /** * Newly-created Nav Menus and Nav Menu Items have negative integer IDs which * serve as placeholders until Save & Publish happens. * * @alias wp.customize.Menus.generatePlaceholderAutoIncrementId * * @return {number} */ api.Menus.generatePlaceholderAutoIncrementId = function() { return -Math.ceil( api.Menus.data.phpIntMax * Math.random() ); }; /** * wp.customize.Menus.AvailableItemModel * * A single available menu item model. See PHP's WP_Customize_Nav_Menu_Item_Setting class. * * @class wp.customize.Menus.AvailableItemModel * @augments Backbone.Model */ api.Menus.AvailableItemModel = Backbone.Model.extend( $.extend( { id: null // This is only used by Backbone. }, api.Menus.data.defaultSettingValues.nav_menu_item ) ); /** * wp.customize.Menus.AvailableItemCollection * * Collection for available menu item models. * * @class wp.customize.Menus.AvailableItemCollection * @augments Backbone.Collection */ api.Menus.AvailableItemCollection = Backbone.Collection.extend(/** @lends wp.customize.Menus.AvailableItemCollection.prototype */{ model: api.Menus.AvailableItemModel, sort_key: 'order', comparator: function( item ) { return -item.get( this.sort_key ); }, sortByField: function( fieldName ) { this.sort_key = fieldName; this.sort(); } }); api.Menus.availableMenuItems = new api.Menus.AvailableItemCollection( api.Menus.data.availableMenuItems ); /** * Insert a new `auto-draft` post. * * @since 4.7.0 * @alias wp.customize.Menus.insertAutoDraftPost * * @param {Object} params - Parameters for the draft post to create. * @param {string} params.post_type - Post type to add. * @param {string} params.post_title - Post title to use. * @return {jQuery.promise} Promise resolved with the added post. */ api.Menus.insertAutoDraftPost = function insertAutoDraftPost( params ) { var request, deferred = $.Deferred(); request = wp.ajax.post( 'customize-nav-menus-insert-auto-draft', { 'customize-menus-nonce': api.settings.nonce['customize-menus'], 'wp_customize': 'on', 'customize_changeset_uuid': api.settings.changeset.uuid, 'params': params } ); request.done( function( response ) { if ( response.post_id ) { api( 'nav_menus_created_posts' ).set( api( 'nav_menus_created_posts' ).get().concat( [ response.post_id ] ) ); if ( 'page' === params.post_type ) { // Activate static front page controls as this could be the first page created. if ( api.section.has( 'static_front_page' ) ) { api.section( 'static_front_page' ).activate(); } // Add new page to dropdown-pages controls. api.control.each( function( control ) { var select; if ( 'dropdown-pages' === control.params.type ) { select = control.container.find( 'select[name^="_customize-dropdown-pages-"]' ); select.append( new Option( params.post_title, response.post_id ) ); } } ); } deferred.resolve( response ); } } ); request.fail( function( response ) { var error = response || ''; if ( 'undefined' !== typeof response.message ) { error = response.message; } console.error( error ); deferred.rejectWith( error ); } ); return deferred.promise(); }; api.Menus.AvailableMenuItemsPanelView = wp.Backbone.View.extend(/** @lends wp.customize.Menus.AvailableMenuItemsPanelView.prototype */{ el: '#available-menu-items', events: { 'input #menu-items-search': 'debounceSearch', 'focus .menu-item-tpl': 'focus', 'click .menu-item-tpl': '_submit', 'click #custom-menu-item-submit': '_submitLink', 'keypress #custom-menu-item-name': '_submitLink', 'click .new-content-item .add-content': '_submitNew', 'keypress .create-item-input': '_submitNew', 'keydown': 'keyboardAccessible' }, // Cache current selected menu item. selected: null, // Cache menu control that opened the panel. currentMenuControl: null, debounceSearch: null, $search: null, $clearResults: null, searchTerm: '', rendered: false, pages: {}, sectionContent: '', loading: false, addingNew: false, /** * wp.customize.Menus.AvailableMenuItemsPanelView * * View class for the available menu items panel. * * @constructs wp.customize.Menus.AvailableMenuItemsPanelView * @augments wp.Backbone.View */ initialize: function() { var self = this; if ( ! api.panel.has( 'nav_menus' ) ) { return; } this.$search = $( '#menu-items-search' ); this.$clearResults = this.$el.find( '.clear-results' ); this.sectionContent = this.$el.find( '.available-menu-items-list' ); this.debounceSearch = _.debounce( self.search, 500 ); _.bindAll( this, 'close' ); /* * If the available menu items panel is open and the customize controls * are interacted with (other than an item being deleted), then close * the available menu items panel. Also close on back button click. */ $( '#customize-controls, .customize-section-back' ).on( 'click keydown', function( e ) { var isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ), isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' ); if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) { self.close(); } } ); // Clear the search results and trigger an `input` event to fire a new search. this.$clearResults.on( 'click', function() { self.$search.val( '' ).trigger( 'focus' ).trigger( 'input' ); } ); this.$el.on( 'input', '#custom-menu-item-name.invalid, #custom-menu-item-url.invalid', function() { $( this ).removeClass( 'invalid' ); }); // Load available items if it looks like we'll need them. api.panel( 'nav_menus' ).container.on( 'expanded', function() { if ( ! self.rendered ) { self.initList(); self.rendered = true; } }); // Load more items. this.sectionContent.on( 'scroll', function() { var totalHeight = self.$el.find( '.accordion-section.open .available-menu-items-list' ).prop( 'scrollHeight' ), visibleHeight = self.$el.find( '.accordion-section.open' ).height(); if ( ! self.loading && $( this ).scrollTop() > 3 / 4 * totalHeight - visibleHeight ) { var type = $( this ).data( 'type' ), object = $( this ).data( 'object' ); if ( 'search' === type ) { if ( self.searchTerm ) { self.doSearch( self.pages.search ); } } else { self.loadItems( [ { type: type, object: object } ] ); } } }); // Close the panel if the URL in the preview changes. api.previewer.bind( 'url', this.close ); self.delegateEvents(); }, // Search input change handler. search: function( event ) { var $searchSection = $( '#available-menu-items-search' ), $otherSections = $( '#available-menu-items .accordion-section' ).not( $searchSection ); if ( ! event ) { return; } if ( this.searchTerm === event.target.value ) { return; } if ( '' !== event.target.value && ! $searchSection.hasClass( 'open' ) ) { $otherSections.fadeOut( 100 ); $searchSection.find( '.accordion-section-content' ).slideDown( 'fast' ); $searchSection.addClass( 'open' ); this.$clearResults.addClass( 'is-visible' ); } else if ( '' === event.target.value ) { $searchSection.removeClass( 'open' ); $otherSections.show(); this.$clearResults.removeClass( 'is-visible' ); } this.searchTerm = event.target.value; this.pages.search = 1; this.doSearch( 1 ); }, // Get search results. doSearch: function( page ) { var self = this, params, $section = $( '#available-menu-items-search' ), $content = $section.find( '.accordion-section-content' ), itemTemplate = wp.template( 'available-menu-item' ); if ( self.currentRequest ) { self.currentRequest.abort(); } if ( page < 0 ) { return; } else if ( page > 1 ) { $section.addClass( 'loading-more' ); $content.attr( 'aria-busy', 'true' ); wp.a11y.speak( api.Menus.data.l10n.itemsLoadingMore ); } else if ( '' === self.searchTerm ) { $content.html( '' ); wp.a11y.speak( '' ); return; } $section.addClass( 'loading' ); self.loading = true; params = api.previewer.query( { excludeCustomizedSaved: true } ); _.extend( params, { 'customize-menus-nonce': api.settings.nonce['customize-menus'], 'wp_customize': 'on', 'search': self.searchTerm, 'page': page } ); self.currentRequest = wp.ajax.post( 'search-available-menu-items-customizer', params ); self.currentRequest.done(function( data ) { var items; if ( 1 === page ) { // Clear previous results as it's a new search. $content.empty(); } $section.removeClass( 'loading loading-more' ); $content.attr( 'aria-busy', 'false' ); $section.addClass( 'open' ); self.loading = false; items = new api.Menus.AvailableItemCollection( data.items ); self.collection.add( items.models ); items.each( function( menuItem ) { $content.append( itemTemplate( menuItem.attributes ) ); } ); if ( 20 > items.length ) { self.pages.search = -1; // Up to 20 posts and 20 terms in results, if <20, no more results for either. } else { self.pages.search = self.pages.search + 1; } if ( items && page > 1 ) { wp.a11y.speak( api.Menus.data.l10n.itemsFoundMore.replace( '%d', items.length ) ); } else if ( items && page === 1 ) { wp.a11y.speak( api.Menus.data.l10n.itemsFound.replace( '%d', items.length ) ); } }); self.currentRequest.fail(function( data ) { // data.message may be undefined, for example when typing slow and the request is aborted. if ( data.message ) { $content.empty().append( $( '
  • ' ).text( data.message ) ); wp.a11y.speak( data.message ); } self.pages.search = -1; }); self.currentRequest.always(function() { $section.removeClass( 'loading loading-more' ); $content.attr( 'aria-busy', 'false' ); self.loading = false; self.currentRequest = null; }); }, // Render the individual items. initList: function() { var self = this; // Render the template for each item by type. _.each( api.Menus.data.itemTypes, function( itemType ) { self.pages[ itemType.type + ':' + itemType.object ] = 0; } ); self.loadItems( api.Menus.data.itemTypes ); }, /** * Load available nav menu items. * * @since 4.3.0 * @since 4.7.0 Changed function signature to take list of item types instead of single type/object. * @access private * * @param {Array.} itemTypes List of objects containing type and key. * @param {string} deprecated Formerly the object parameter. * @return {void} */ loadItems: function( itemTypes, deprecated ) { var self = this, _itemTypes, requestItemTypes = [], params, request, itemTemplate, availableMenuItemContainers = {}; itemTemplate = wp.template( 'available-menu-item' ); if ( _.isString( itemTypes ) && _.isString( deprecated ) ) { _itemTypes = [ { type: itemTypes, object: deprecated } ]; } else { _itemTypes = itemTypes; } _.each( _itemTypes, function( itemType ) { var container, name = itemType.type + ':' + itemType.object; if ( -1 === self.pages[ name ] ) { return; // Skip types for which there are no more results. } container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object ); container.find( '.accordion-section-title' ).addClass( 'loading' ); availableMenuItemContainers[ name ] = container; requestItemTypes.push( { object: itemType.object, type: itemType.type, page: self.pages[ name ] } ); } ); if ( 0 === requestItemTypes.length ) { return; } self.loading = true; params = api.previewer.query( { excludeCustomizedSaved: true } ); _.extend( params, { 'customize-menus-nonce': api.settings.nonce['customize-menus'], 'wp_customize': 'on', 'item_types': requestItemTypes } ); request = wp.ajax.post( 'load-available-menu-items-customizer', params ); request.done(function( data ) { var typeInner; _.each( data.items, function( typeItems, name ) { if ( 0 === typeItems.length ) { if ( 0 === self.pages[ name ] ) { availableMenuItemContainers[ name ].find( '.accordion-section-title' ) .addClass( 'cannot-expand' ) .removeClass( 'loading' ) .find( '.accordion-section-title > button' ) .prop( 'tabIndex', -1 ); } self.pages[ name ] = -1; return; } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) { availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).trigger( 'click' ); } typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away? self.collection.add( typeItems.models ); typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' ); typeItems.each( function( menuItem ) { typeInner.append( itemTemplate( menuItem.attributes ) ); } ); self.pages[ name ] += 1; }); }); request.fail(function( data ) { if ( typeof console !== 'undefined' && console.error ) { console.error( data ); } }); request.always(function() { _.each( availableMenuItemContainers, function( container ) { container.find( '.accordion-section-title' ).removeClass( 'loading' ); } ); self.loading = false; }); }, // Adjust the height of each section of items to fit the screen. itemSectionHeight: function() { var sections, lists, totalHeight, accordionHeight, diff; totalHeight = window.innerHeight; sections = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .accordion-section-content' ); lists = this.$el.find( '.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")' ); accordionHeight = 46 * ( 1 + sections.length ) + 14; // Magic numbers. diff = totalHeight - accordionHeight; if ( 120 < diff && 290 > diff ) { sections.css( 'max-height', diff ); lists.css( 'max-height', ( diff - 60 ) ); } }, // Highlights a menu item. select: function( menuitemTpl ) { this.selected = $( menuitemTpl ); this.selected.siblings( '.menu-item-tpl' ).removeClass( 'selected' ); this.selected.addClass( 'selected' ); }, // Highlights a menu item on focus. focus: function( event ) { this.select( $( event.currentTarget ) ); }, // Submit handler for keypress and click on menu item. _submit: function( event ) { // Only proceed with keypress if it is Enter or Spacebar. if ( 'keypress' === event.type && ( 13 !== event.which && 32 !== event.which ) ) { return; } this.submit( $( event.currentTarget ) ); }, // Adds a selected menu item to the menu. submit: function( menuitemTpl ) { var menuitemId, menu_item; if ( ! menuitemTpl ) { menuitemTpl = this.selected; } if ( ! menuitemTpl || ! this.currentMenuControl ) { return; } this.select( menuitemTpl ); menuitemId = $( this.selected ).data( 'menu-item-id' ); menu_item = this.collection.findWhere( { id: menuitemId } ); if ( ! menu_item ) { return; } this.currentMenuControl.addItemToMenu( menu_item.attributes ); $( menuitemTpl ).find( '.menu-item-handle' ).addClass( 'item-added' ); }, // Submit handler for keypress and click on custom menu item. _submitLink: function( event ) { // Only proceed with keypress if it is Enter. if ( 'keypress' === event.type && 13 !== event.which ) { return; } this.submitLink(); }, // Adds the custom menu item to the menu. submitLink: function() { var menuItem, itemName = $( '#custom-menu-item-name' ), itemUrl = $( '#custom-menu-item-url' ), url = itemUrl.val().trim(), urlRegex; if ( ! this.currentMenuControl ) { return; } /* * Allow URLs including: * - http://example.com/ * - //example.com * - /directory/ * - ?query-param * - #target * - mailto:foo@example.com * * Any further validation will be handled on the server when the setting is attempted to be saved, * so this pattern does not need to be complete. */ urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/; if ( '' === itemName.val() ) { itemName.addClass( 'invalid' ); return; } else if ( ! urlRegex.test( url ) ) { itemUrl.addClass( 'invalid' ); return; } menuItem = { 'title': itemName.val(), 'url': url, 'type': 'custom', 'type_label': api.Menus.data.l10n.custom_label, 'object': 'custom' }; this.currentMenuControl.addItemToMenu( menuItem ); // Reset the custom link form. itemUrl.val( '' ).attr( 'placeholder', 'https://' ); itemName.val( '' ); }, /** * Submit handler for keypress (enter) on field and click on button. * * @since 4.7.0 * @private * * @param {jQuery.Event} event Event. * @return {void} */ _submitNew: function( event ) { var container; // Only proceed with keypress if it is Enter. if ( 'keypress' === event.type && 13 !== event.which ) { return; } if ( this.addingNew ) { return; } container = $( event.target ).closest( '.accordion-section' ); this.submitNew( container ); }, /** * Creates a new object and adds an associated menu item to the menu. * * @since 4.7.0 * @private * * @param {jQuery} container * @return {void} */ submitNew: function( container ) { var panel = this, itemName = container.find( '.create-item-input' ), title = itemName.val(), dataContainer = container.find( '.available-menu-items-list' ), itemType = dataContainer.data( 'type' ), itemObject = dataContainer.data( 'object' ), itemTypeLabel = dataContainer.data( 'type_label' ), promise; if ( ! this.currentMenuControl ) { return; } // Only posts are supported currently. if ( 'post_type' !== itemType ) { return; } if ( '' === itemName.val().trim() ) { itemName.addClass( 'invalid' ); itemName.focus(); return; } else { itemName.removeClass( 'invalid' ); container.find( '.accordion-section-title' ).addClass( 'loading' ); } panel.addingNew = true; itemName.attr( 'disabled', 'disabled' ); promise = api.Menus.insertAutoDraftPost( { post_title: title, post_type: itemObject } ); promise.done( function( data ) { var availableItem, $content, itemElement; availableItem = new api.Menus.AvailableItemModel( { 'id': 'post-' + data.post_id, // Used for available menu item Backbone models. 'title': itemName.val(), 'type': itemType, 'type_label': itemTypeLabel, 'object': itemObject, 'object_id': data.post_id, 'url': data.url } ); // Add new item to menu. panel.currentMenuControl.addItemToMenu( availableItem.attributes ); // Add the new item to the list of available items. api.Menus.availableMenuItemsPanel.collection.add( availableItem ); $content = container.find( '.available-menu-items-list' ); itemElement = $( wp.template( 'available-menu-item' )( availableItem.attributes ) ); itemElement.find( '.menu-item-handle:first' ).addClass( 'item-added' ); $content.prepend( itemElement ); $content.scrollTop(); // Reset the create content form. itemName.val( '' ).removeAttr( 'disabled' ); panel.addingNew = false; container.find( '.accordion-section-title' ).removeClass( 'loading' ); } ); }, // Opens the panel. open: function( menuControl ) { var panel = this, close; this.currentMenuControl = menuControl; this.itemSectionHeight(); if ( api.section.has( 'publish_settings' ) ) { api.section( 'publish_settings' ).collapse(); } $( 'body' ).addClass( 'adding-menu-items' ); close = function() { panel.close(); $( this ).off( 'click', close ); }; $( '#customize-preview' ).on( 'click', close ); // Collapse all controls. _( this.currentMenuControl.getMenuItemControls() ).each( function( control ) { control.collapseForm(); } ); this.$el.find( '.selected' ).removeClass( 'selected' ); this.$search.trigger( 'focus' ); }, // Closes the panel. close: function( options ) { options = options || {}; if ( options.returnFocus && this.currentMenuControl ) { this.currentMenuControl.container.find( '.add-new-menu-item' ).focus(); } this.currentMenuControl = null; this.selected = null; $( 'body' ).removeClass( 'adding-menu-items' ); $( '#available-menu-items .menu-item-handle.item-added' ).removeClass( 'item-added' ); this.$search.val( '' ).trigger( 'input' ); }, // Add a few keyboard enhancements to the panel. keyboardAccessible: function( event ) { var isEnter = ( 13 === event.which ), isEsc = ( 27 === event.which ), isBackTab = ( 9 === event.which && event.shiftKey ), isSearchFocused = $( event.target ).is( this.$search ); // If enter pressed but nothing entered, don't do anything. if ( isEnter && ! this.$search.val() ) { return; } if ( isSearchFocused && isBackTab ) { this.currentMenuControl.container.find( '.add-new-menu-item' ).focus(); event.preventDefault(); // Avoid additional back-tab. } else if ( isEsc ) { this.close( { returnFocus: true } ); } } }); /** * wp.customize.Menus.MenusPanel * * Customizer panel for menus. This is used only for screen options management. * Note that 'menus' must match the WP_Customize_Menu_Panel::$type. * * @class wp.customize.Menus.MenusPanel * @augments wp.customize.Panel */ api.Menus.MenusPanel = api.Panel.extend(/** @lends wp.customize.Menus.MenusPanel.prototype */{ attachEvents: function() { api.Panel.prototype.attachEvents.call( this ); var panel = this, panelMeta = panel.container.find( '.panel-meta' ), help = panelMeta.find( '.customize-help-toggle' ), content = panelMeta.find( '.customize-panel-description' ), options = $( '#screen-options-wrap' ), button = panelMeta.find( '.customize-screen-options-toggle' ); button.on( 'click keydown', function( event ) { if ( api.utils.isKeydownButNotEnterEvent( event ) ) { return; } event.preventDefault(); // Hide description. if ( content.not( ':hidden' ) ) { content.slideUp( 'fast' ); help.attr( 'aria-expanded', 'false' ); } if ( 'true' === button.attr( 'aria-expanded' ) ) { button.attr( 'aria-expanded', 'false' ); panelMeta.removeClass( 'open' ); panelMeta.removeClass( 'active-menu-screen-options' ); options.slideUp( 'fast' ); } else { button.attr( 'aria-expanded', 'true' ); panelMeta.addClass( 'open' ); panelMeta.addClass( 'active-menu-screen-options' ); options.slideDown( 'fast' ); } return false; } ); // Help toggle. help.on( 'click keydown', function( event ) { if ( api.utils.isKeydownButNotEnterEvent( event ) ) { return; } event.preventDefault(); if ( 'true' === button.attr( 'aria-expanded' ) ) { button.attr( 'aria-expanded', 'false' ); help.attr( 'aria-expanded', 'true' ); panelMeta.addClass( 'open' ); panelMeta.removeClass( 'active-menu-screen-options' ); options.slideUp( 'fast' ); content.slideDown( 'fast' ); } } ); }, /** * Update field visibility when clicking on the field toggles. */ ready: function() { var panel = this; panel.container.find( '.hide-column-tog' ).on( 'click', function() { panel.saveManageColumnsState(); }); // Inject additional heading into the menu locations section's head container. api.section( 'menu_locations', function( section ) { section.headContainer.prepend( wp.template( 'nav-menu-locations-header' )( api.Menus.data ) ); } ); }, /** * Save hidden column states. * * @since 4.3.0 * @private * * @return {void} */ saveManageColumnsState: _.debounce( function() { var panel = this; if ( panel._updateHiddenColumnsRequest ) { panel._updateHiddenColumnsRequest.abort(); } panel._updateHiddenColumnsRequest = wp.ajax.post( 'hidden-columns', { hidden: panel.hidden(), screenoptionnonce: $( '#screenoptionnonce' ).val(), page: 'nav-menus' } ); panel._updateHiddenColumnsRequest.always( function() { panel._updateHiddenColumnsRequest = null; } ); }, 2000 ), /** * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers. */ checked: function() {}, /** * @deprecated Since 4.7.0 now that the nav_menu sections are responsible for toggling the classes on their own containers. */ unchecked: function() {}, /** * Get hidden fields. * * @since 4.3.0 * @private * * @return {Array} Fields (columns) that are hidden. */ hidden: function() { return $( '.hide-column-tog' ).not( ':checked' ).map( function() { var id = this.id; return id.substring( 0, id.length - 5 ); }).get().join( ',' ); } } ); /** * wp.customize.Menus.MenuSection * * Customizer section for menus. This is used only for lazy-loading child controls. * Note that 'nav_menu' must match the WP_Customize_Menu_Section::$type. * * @class wp.customize.Menus.MenuSection * @augments wp.customize.Section */ api.Menus.MenuSection = api.Section.extend(/** @lends wp.customize.Menus.MenuSection.prototype */{ /** * Initialize. * * @since 4.3.0 * * @param {string} id * @param {Object} options */ initialize: function( id, options ) { var section = this; api.Section.prototype.initialize.call( section, id, options ); section.deferred.initSortables = $.Deferred(); }, /** * Ready. */ ready: function() { var section = this, fieldActiveToggles, handleFieldActiveToggle; if ( 'undefined' === typeof section.params.menu_id ) { throw new Error( 'params.menu_id was not defined' ); } /* * Since newly created sections won't be registered in PHP, we need to prevent the * preview's sending of the activeSections to result in this control * being deactivated when the preview refreshes. So we can hook onto * the setting that has the same ID and its presence can dictate * whether the section is active. */ section.active.validate = function() { if ( ! api.has( section.id ) ) { return false; } return !! api( section.id ).get(); }; section.populateControls(); section.navMenuLocationSettings = {}; section.assignedLocations = new api.Value( [] ); api.each(function( setting, id ) { var matches = id.match( /^nav_menu_locations\[(.+?)]/ ); if ( matches ) { section.navMenuLocationSettings[ matches[1] ] = setting; setting.bind( function() { section.refreshAssignedLocations(); }); } }); section.assignedLocations.bind(function( to ) { section.updateAssignedLocationsInSectionTitle( to ); }); section.refreshAssignedLocations(); api.bind( 'pane-contents-reflowed', function() { // Skip menus that have been removed. if ( ! section.contentContainer.parent().length ) { return; } section.container.find( '.menu-item .menu-item-reorder-nav button' ).attr({ 'tabindex': '0', 'aria-hidden': 'false' }); section.container.find( '.menu-item.move-up-disabled .menus-move-up' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); section.container.find( '.menu-item.move-down-disabled .menus-move-down' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); section.container.find( '.menu-item.move-left-disabled .menus-move-left' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); section.container.find( '.menu-item.move-right-disabled .menus-move-right' ).attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); } ); /** * Update the active field class for the content container for a given checkbox toggle. * * @this {jQuery} * @return {void} */ handleFieldActiveToggle = function() { var className = 'field-' + $( this ).val() + '-active'; section.contentContainer.toggleClass( className, $( this ).prop( 'checked' ) ); }; fieldActiveToggles = api.panel( 'nav_menus' ).contentContainer.find( '.metabox-prefs:first' ).find( '.hide-column-tog' ); fieldActiveToggles.each( handleFieldActiveToggle ); fieldActiveToggles.on( 'click', handleFieldActiveToggle ); }, populateControls: function() { var section = this, menuNameControlId, menuLocationsControlId, menuAutoAddControlId, menuDeleteControlId, menuControl, menuNameControl, menuLocationsControl, menuAutoAddControl, menuDeleteControl; // Add the control for managing the menu name. menuNameControlId = section.id + '[name]'; menuNameControl = api.control( menuNameControlId ); if ( ! menuNameControl ) { menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, { type: 'nav_menu_name', label: api.Menus.data.l10n.menuNameLabel, section: section.id, priority: 0, settings: { 'default': section.id } } ); api.control.add( menuNameControl ); menuNameControl.active.set( true ); } // Add the menu control. menuControl = api.control( section.id ); if ( ! menuControl ) { menuControl = new api.controlConstructor.nav_menu( section.id, { type: 'nav_menu', section: section.id, priority: 998, settings: { 'default': section.id }, menu_id: section.params.menu_id } ); api.control.add( menuControl ); menuControl.active.set( true ); } // Add the menu locations control. menuLocationsControlId = section.id + '[locations]'; menuLocationsControl = api.control( menuLocationsControlId ); if ( ! menuLocationsControl ) { menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, { section: section.id, priority: 999, settings: { 'default': section.id }, menu_id: section.params.menu_id } ); api.control.add( menuLocationsControl.id, menuLocationsControl ); menuControl.active.set( true ); } // Add the control for managing the menu auto_add. menuAutoAddControlId = section.id + '[auto_add]'; menuAutoAddControl = api.control( menuAutoAddControlId ); if ( ! menuAutoAddControl ) { menuAutoAddControl = new api.controlConstructor.nav_menu_auto_add( menuAutoAddControlId, { type: 'nav_menu_auto_add', label: '', section: section.id, priority: 1000, settings: { 'default': section.id } } ); api.control.add( menuAutoAddControl ); menuAutoAddControl.active.set( true ); } // Add the control for deleting the menu. menuDeleteControlId = section.id + '[delete]'; menuDeleteControl = api.control( menuDeleteControlId ); if ( ! menuDeleteControl ) { menuDeleteControl = new api.Control( menuDeleteControlId, { section: section.id, priority: 1001, templateId: 'nav-menu-delete-button' } ); api.control.add( menuDeleteControl.id, menuDeleteControl ); menuDeleteControl.active.set( true ); menuDeleteControl.deferred.embedded.done( function () { menuDeleteControl.container.find( 'button' ).on( 'click', function() { var menuId = section.params.menu_id; var menuControl = api.Menus.getMenuControl( menuId ); menuControl.setting.set( false ); }); } ); } }, /** * */ refreshAssignedLocations: function() { var section = this, menuTermId = section.params.menu_id, currentAssignedLocations = []; _.each( section.navMenuLocationSettings, function( setting, themeLocation ) { if ( setting() === menuTermId ) { currentAssignedLocations.push( themeLocation ); } }); section.assignedLocations.set( currentAssignedLocations ); }, /** * @param {Array} themeLocationSlugs Theme location slugs. */ updateAssignedLocationsInSectionTitle: function( themeLocationSlugs ) { var section = this, $title; $title = section.container.find( '.accordion-section-title button:first' ); $title.find( '.menu-in-location' ).remove(); _.each( themeLocationSlugs, function( themeLocationSlug ) { var $label, locationName; $label = $( '' ); locationName = api.Menus.data.locationSlugMappedToName[ themeLocationSlug ]; $label.text( api.Menus.data.l10n.menuLocation.replace( '%s', locationName ) ); $title.append( $label ); }); section.container.toggleClass( 'assigned-to-menu-location', 0 !== themeLocationSlugs.length ); }, onChangeExpanded: function( expanded, args ) { var section = this, completeCallback; if ( expanded ) { wpNavMenu.menuList = section.contentContainer; wpNavMenu.targetList = wpNavMenu.menuList; // Add attributes needed by wpNavMenu. $( '#menu-to-edit' ).removeAttr( 'id' ); wpNavMenu.menuList.attr( 'id', 'menu-to-edit' ).addClass( 'menu' ); api.Menus.MenuItemControl.prototype.initAccessibility(); _.each( api.section( section.id ).controls(), function( control ) { if ( 'nav_menu_item' === control.params.type ) { control.actuallyEmbed(); } } ); // Make sure Sortables is initialized after the section has been expanded to prevent `offset` issues. if ( args.completeCallback ) { completeCallback = args.completeCallback; } args.completeCallback = function() { if ( 'resolved' !== section.deferred.initSortables.state() ) { wpNavMenu.initSortables(); // Depends on menu-to-edit ID being set above. section.deferred.initSortables.resolve( wpNavMenu.menuList ); // Now MenuControl can extend the sortable. // @todo Note that wp.customize.reflowPaneContents() is debounced, // so this immediate change will show a slight flicker while priorities get updated. api.control( 'nav_menu[' + String( section.params.menu_id ) + ']' ).reflowMenuItems(); } if ( _.isFunction( completeCallback ) ) { completeCallback(); } }; } api.Section.prototype.onChangeExpanded.call( section, expanded, args ); }, /** * Highlight how a user may create new menu items. * * This method reminds the user to create new menu items and how. * It's exposed this way because this class knows best which UI needs * highlighted but those expanding this section know more about why and * when the affordance should be highlighted. * * @since 4.9.0 * * @return {void} */ highlightNewItemButton: function() { api.utils.highlightButton( this.contentContainer.find( '.add-new-menu-item' ), { delay: 2000 } ); } }); /** * Create a nav menu setting and section. * * @since 4.9.0 * * @param {string} [name=''] Nav menu name. * @return {wp.customize.Menus.MenuSection} Added nav menu. */ api.Menus.createNavMenu = function createNavMenu( name ) { var customizeId, placeholderId, setting; placeholderId = api.Menus.generatePlaceholderAutoIncrementId(); customizeId = 'nav_menu[' + String( placeholderId ) + ']'; // Register the menu control setting. setting = api.create( customizeId, customizeId, {}, { type: 'nav_menu', transport: api.Menus.data.settingTransport, previewer: api.previewer } ); setting.set( $.extend( {}, api.Menus.data.defaultSettingValues.nav_menu, { name: name || '' } ) ); /* * Add the menu section (and its controls). * Note that this will automatically create the required controls * inside via the Section's ready method. */ return api.section.add( new api.Menus.MenuSection( customizeId, { panel: 'nav_menus', title: displayNavMenuName( name ), customizeAction: api.Menus.data.l10n.customizingMenus, priority: 10, menu_id: placeholderId } ) ); }; /** * wp.customize.Menus.NewMenuSection * * Customizer section for new menus. * * @class wp.customize.Menus.NewMenuSection * @augments wp.customize.Section */ api.Menus.NewMenuSection = api.Section.extend(/** @lends wp.customize.Menus.NewMenuSection.prototype */{ /** * Add behaviors for the accordion section. * * @since 4.3.0 */ attachEvents: function() { var section = this, container = section.container, contentContainer = section.contentContainer, navMenuSettingPattern = /^nav_menu\[/; section.headContainer.find( '.accordion-section-title' ).replaceWith( wp.template( 'nav-menu-create-menu-section-title' ) ); /* * We have to manually handle section expanded because we do not * apply the `accordion-section-title` class to this button-driven section. */ container.on( 'click', '.customize-add-menu-button', function() { section.expand(); }); contentContainer.on( 'keydown', '.menu-name-field', function( event ) { if ( 13 === event.which ) { // Enter. section.submit(); } } ); contentContainer.on( 'click', '#customize-new-menu-submit', function( event ) { section.submit(); event.stopPropagation(); event.preventDefault(); } ); /** * Get number of non-deleted nav menus. * * @since 4.9.0 * @return {number} Count. */ function getNavMenuCount() { var count = 0; api.each( function( setting ) { if ( navMenuSettingPattern.test( setting.id ) && false !== setting.get() ) { count += 1; } } ); return count; } /** * Update visibility of notice to prompt users to create menus. * * @since 4.9.0 * @return {void} */ function updateNoticeVisibility() { container.find( '.add-new-menu-notice' ).prop( 'hidden', getNavMenuCount() > 0 ); } /** * Handle setting addition. * * @since 4.9.0 * @param {wp.customize.Setting} setting - Added setting. * @return {void} */ function addChangeEventListener( setting ) { if ( navMenuSettingPattern.test( setting.id ) ) { setting.bind( updateNoticeVisibility ); updateNoticeVisibility(); } } /** * Handle setting removal. * * @since 4.9.0 * @param {wp.customize.Setting} setting - Removed setting. * @return {void} */ function removeChangeEventListener( setting ) { if ( navMenuSettingPattern.test( setting.id ) ) { setting.unbind( updateNoticeVisibility ); updateNoticeVisibility(); } } api.each( addChangeEventListener ); api.bind( 'add', addChangeEventListener ); api.bind( 'removed', removeChangeEventListener ); updateNoticeVisibility(); api.Section.prototype.attachEvents.apply( section, arguments ); }, /** * Set up the control. * * @since 4.9.0 */ ready: function() { this.populateControls(); }, /** * Create the controls for this section. * * @since 4.9.0 */ populateControls: function() { var section = this, menuNameControlId, menuLocationsControlId, newMenuSubmitControlId, menuNameControl, menuLocationsControl, newMenuSubmitControl; menuNameControlId = section.id + '[name]'; menuNameControl = api.control( menuNameControlId ); if ( ! menuNameControl ) { menuNameControl = new api.controlConstructor.nav_menu_name( menuNameControlId, { label: api.Menus.data.l10n.menuNameLabel, description: api.Menus.data.l10n.newMenuNameDescription, section: section.id, priority: 0 } ); api.control.add( menuNameControl.id, menuNameControl ); menuNameControl.active.set( true ); } menuLocationsControlId = section.id + '[locations]'; menuLocationsControl = api.control( menuLocationsControlId ); if ( ! menuLocationsControl ) { menuLocationsControl = new api.controlConstructor.nav_menu_locations( menuLocationsControlId, { section: section.id, priority: 1, menu_id: '', isCreating: true } ); api.control.add( menuLocationsControlId, menuLocationsControl ); menuLocationsControl.active.set( true ); } newMenuSubmitControlId = section.id + '[submit]'; newMenuSubmitControl = api.control( newMenuSubmitControlId ); if ( !newMenuSubmitControl ) { newMenuSubmitControl = new api.Control( newMenuSubmitControlId, { section: section.id, priority: 1, templateId: 'nav-menu-submit-new-button' } ); api.control.add( newMenuSubmitControlId, newMenuSubmitControl ); newMenuSubmitControl.active.set( true ); } }, /** * Create the new menu with name and location supplied by the user. * * @since 4.9.0 */ submit: function() { var section = this, contentContainer = section.contentContainer, nameInput = contentContainer.find( '.menu-name-field' ).first(), name = nameInput.val(), menuSection; if ( ! name ) { nameInput.addClass( 'invalid' ); nameInput.focus(); return; } menuSection = api.Menus.createNavMenu( name ); // Clear name field. nameInput.val( '' ); nameInput.removeClass( 'invalid' ); contentContainer.find( '.assigned-menu-location input[type=checkbox]' ).each( function() { var checkbox = $( this ), navMenuLocationSetting; if ( checkbox.prop( 'checked' ) ) { navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' ); navMenuLocationSetting.set( menuSection.params.menu_id ); // Reset state for next new menu. checkbox.prop( 'checked', false ); } } ); wp.a11y.speak( api.Menus.data.l10n.menuAdded ); // Focus on the new menu section. menuSection.focus( { completeCallback: function() { menuSection.highlightNewItemButton(); } } ); }, /** * Select a default location. * * This method selects a single location by default so we can support * creating a menu for a specific menu location. * * @since 4.9.0 * * @param {string|null} locationId - The ID of the location to select. `null` clears all selections. * @return {void} */ selectDefaultLocation: function( locationId ) { var locationControl = api.control( this.id + '[locations]' ), locationSelections = {}; if ( locationId !== null ) { locationSelections[ locationId ] = true; } locationControl.setSelections( locationSelections ); } }); /** * wp.customize.Menus.MenuLocationControl * * Customizer control for menu locations (rendered as a ').children("input").on("keydown",function(t){var e=t.which;13===e&&(t.preventDefault(),p.children(".save").trigger("click")),27===e&&p.children(".cancel").trigger("click")}).on("keyup",function(){s.val(this.value)}).trigger("focus")}),window.wptitlehint=function(t){var e=h("#"+(t=t||"title")),i=h("#"+t+"-prompt-text");""===e.val()&&i.removeClass("screen-reader-text"),e.on("input",function(){""===this.value?i.removeClass("screen-reader-text"):i.addClass("screen-reader-text")})},wptitlehint(),t=h("#post-status-info"),c=h("#postdivrich"),!u.length||"ontouchstart"in window?h("#content-resize-handle").hide():t.on("mousedown.wp-editor-resize",function(t){(o="undefined"!=typeof tinymce?tinymce.get("content"):o)&&!o.isHidden()?(r=!0,l=h("#content_ifr").height()-t.pageY):(r=!1,l=u.height()-t.pageY,u.trigger("blur")),v.on("mousemove.wp-editor-resize",D).on("mouseup.wp-editor-resize mouseleave.wp-editor-resize",j),t.preventDefault()}).on("mouseup.wp-editor-resize",j),"undefined"!=typeof tinymce&&(h("#post-formats-select input.post-format").on("change.set-editor-class",function(){var t,e,i=this.id;i&&h(this).prop("checked")&&(t=tinymce.get("content"))&&((e=t.getBody()).className=e.className.replace(/\bpost-format-[^ ]+/,""),t.dom.addClass(e,"post-format-0"==i?"post-format-standard":i),h(document).trigger("editor-classchange"))}),h("#page_template").on("change.set-editor-class",function(){var t,e,i=h(this).val()||"";(i=i.substr(i.lastIndexOf("/")+1,i.length).replace(/\.php$/,"").replace(/\./g,"-"))&&(t=tinymce.get("content"))&&((e=t.getBody()).className=e.className.replace(/\bpage-template-[^ ]+/,""),t.dom.addClass(e,"page-template-"+i),h(document).trigger("editor-classchange"))})),u.on("keydown.wp-autosave",function(t){83!==t.which||t.shiftKey||t.altKey||_&&(!t.metaKey||t.ctrlKey)||!_&&!t.ctrlKey||(wp.autosave&&wp.autosave.server.triggerSave(),t.preventDefault())}),"auto-draft"===h("#original_post_status").val()&&window.history.replaceState&&h("#publish").on("click",function(){p=(p=window.location.href)+(-1!==p.indexOf("?")?"&":"?")+"wp-post-new-reload=true",window.history.replaceState(null,null,p)}),y.on("success",function(t){var e=h(t.trigger),i=h(".success",e.closest(".copy-to-clipboard-container"));t.clearSelection(),clearTimeout(s),i.removeClass("hidden"),s=setTimeout(function(){i.addClass("hidden")},3e3),wp.a11y.speak(x("The file URL has been copied to your clipboard"))})}),function(t,o){t(function(){var i,e=t("#content"),a=t("#wp-word-count").find(".word-count"),n=0;function s(){var t=!i||i.isHidden()?e.val():i.getContent({format:"raw"}),t=o.count(t);t!==n&&a.text(t),n=t}t(document).on("tinymce-editor-init",function(t,e){"content"===e.id&&(i=e).on("nodechange keyup",_.debounce(s,1e3))}),e.on("input keyup",_.debounce(s,1e3)),s()})}(jQuery,new wp.utils.WordCounter);PK1YZF11postbox.min.jsnu[/*! This file is auto-generated */ !function(l){var a=l(document),r=wp.i18n.__;window.postboxes={handle_click:function(){var e,o=l(this),s=o.closest(".postbox"),t=s.attr("id");"dashboard_browser_nag"!==t&&(s.toggleClass("closed"),e=!s.hasClass("closed"),(o.hasClass("handlediv")?o:o.closest(".postbox").find("button.handlediv")).attr("aria-expanded",e),"press-this"!==postboxes.page&&postboxes.save_state(postboxes.page),t&&(s.hasClass("closed")||"function"!=typeof postboxes.pbshow?s.hasClass("closed")&&"function"==typeof postboxes.pbhide&&postboxes.pbhide(t):postboxes.pbshow(t)),a.trigger("postbox-toggled",s))},handleOrder:function(){var e=l(this),o=e.closest(".postbox"),s=o.attr("id"),t=o.closest(".meta-box-sortables").find(".postbox:visible"),a=t.length,t=t.index(o);if("dashboard_browser_nag"!==s)if("true"===e.attr("aria-disabled"))s=e.hasClass("handle-order-higher")?r("The box is on the first position"):r("The box is on the last position"),wp.a11y.speak(s);else{if(e.hasClass("handle-order-higher")){if(0===t)return void postboxes.handleOrderBetweenSortables("previous",e,o);o.prevAll(".postbox:visible").eq(0).before(o),e.trigger("focus"),postboxes.updateOrderButtonsProperties(),postboxes.save_order(postboxes.page)}e.hasClass("handle-order-lower")&&(t+1===a?postboxes.handleOrderBetweenSortables("next",e,o):(o.nextAll(".postbox:visible").eq(0).after(o),e.trigger("focus"),postboxes.updateOrderButtonsProperties(),postboxes.save_order(postboxes.page)))}},handleOrderBetweenSortables:function(e,o,s){var t=o.closest(".meta-box-sortables").attr("id"),a=[];l(".meta-box-sortables:visible").each(function(){a.push(l(this).attr("id"))}),1!==a.length&&(t=l.inArray(t,a),s=s.detach(),"previous"===e&&l(s).appendTo("#"+a[t-1]),"next"===e&&l(s).prependTo("#"+a[t+1]),postboxes._mark_area(),o.focus(),postboxes.updateOrderButtonsProperties(),postboxes.save_order(postboxes.page))},updateOrderButtonsProperties:function(){var e=l(".meta-box-sortables:visible:first").attr("id"),o=l(".meta-box-sortables:visible:last").attr("id"),s=l(".postbox:visible:first"),t=l(".postbox:visible:last"),a=s.attr("id"),r=t.attr("id"),i=s.closest(".meta-box-sortables").attr("id"),t=t.closest(".meta-box-sortables").attr("id"),n=l(".handle-order-higher"),d=l(".handle-order-lower");n.attr("aria-disabled","false").removeClass("hidden"),d.attr("aria-disabled","false").removeClass("hidden"),e===o&&a===r&&(n.addClass("hidden"),d.addClass("hidden")),e===i&&l(s).find(".handle-order-higher").attr("aria-disabled","true"),o===t&&l(".postbox:visible .handle-order-lower").last().attr("aria-disabled","true")},add_postbox_toggles:function(t,e){var o=l(".postbox .hndle, .postbox .handlediv"),s=l(".postbox .handle-order-higher, .postbox .handle-order-lower");this.page=t,this.init(t,e),o.on("click.postboxes",this.handle_click),s.on("click.postboxes",this.handleOrder),l(".postbox .hndle a").on("click",function(e){e.stopPropagation()}),l(".postbox a.dismiss").on("click.postboxes",function(e){var o=l(this).parents(".postbox").attr("id")+"-hide";e.preventDefault(),l("#"+o).prop("checked",!1).triggerHandler("click")}),l(".hide-postbox-tog").on("click.postboxes",function(){var e=l(this),o=e.val(),s=l("#"+o);e.prop("checked")?(s.show(),"function"==typeof postboxes.pbshow&&postboxes.pbshow(o)):(s.hide(),"function"==typeof postboxes.pbhide&&postboxes.pbhide(o)),postboxes.save_state(t),postboxes._mark_area(),a.trigger("postbox-toggled",s)}),l('.columns-prefs input[type="radio"]').on("click.postboxes",function(){var e=parseInt(l(this).val(),10);e&&(postboxes._pb_edit(e),postboxes.save_order(t))})},init:function(o,e){var s=l(document.body).hasClass("mobile"),t=l(".postbox .handlediv");l.extend(this,e||{}),l(".meta-box-sortables").sortable({placeholder:"sortable-placeholder",connectWith:".meta-box-sortables",items:".postbox",handle:".hndle",cursor:"move",delay:s?200:0,distance:2,tolerance:"pointer",forcePlaceholderSize:!0,helper:function(e,o){return o.clone().find(":input").attr("name",function(e,o){return"sort_"+parseInt(1e5*Math.random(),10).toString()+"_"+o}).end()},opacity:.65,start:function(){l("body").addClass("is-dragging-metaboxes"),l(".meta-box-sortables").sortable("refreshPositions")},stop:function(){var e=l(this);l("body").removeClass("is-dragging-metaboxes"),e.find("#dashboard_browser_nag").is(":visible")&&"dashboard_browser_nag"!=this.firstChild.id?e.sortable("cancel"):(postboxes.updateOrderButtonsProperties(),postboxes.save_order(o))},receive:function(e,o){"dashboard_browser_nag"==o.item[0].id&&l(o.sender).sortable("cancel"),postboxes._mark_area(),a.trigger("postbox-moved",o.item)}}),s&&(l(document.body).on("orientationchange.postboxes",function(){postboxes._pb_change()}),this._pb_change()),this._mark_area(),this.updateOrderButtonsProperties(),a.on("postbox-toggled",this.updateOrderButtonsProperties),t.each(function(){var e=l(this);e.attr("aria-expanded",!e.closest(".postbox").hasClass("closed"))})},save_state:function(e){var o,s;"nav-menus"!==e&&(o=l(".postbox").filter(".closed").map(function(){return this.id}).get().join(","),s=l(".postbox").filter(":hidden").map(function(){return this.id}).get().join(","),l.post(ajaxurl,{action:"closed-postboxes",closed:o,hidden:s,closedpostboxesnonce:jQuery("#closedpostboxesnonce").val(),page:e}))},save_order:function(e){var o=l(".columns-prefs input:checked").val()||0,s={action:"meta-box-order",_ajax_nonce:l("#meta-box-order-nonce").val(),page_columns:o,page:e};l(".meta-box-sortables").each(function(){s["order["+this.id.split("-")[0]+"]"]=l(this).sortable("toArray").join(",")}),l.post(ajaxurl,s,function(e){e.success&&wp.a11y.speak(r("The boxes order has been saved."))})},_mark_area:function(){var o=l("div.postbox:visible").length,e=l("#dashboard-widgets .meta-box-sortables:visible, #post-body .meta-box-sortables:visible"),s=!0;e.each(function(){var e=l(this);1==o||e.children(".postbox:visible").length?(e.removeClass("empty-container"),s=!1):e.addClass("empty-container")}),postboxes.updateEmptySortablesText(e,s)},updateEmptySortablesText:function(e,o){var s=l("#dashboard-widgets").length,t=r(o?"Add boxes from the Screen Options menu":"Drag boxes here");s&&e.each(function(){l(this).hasClass("empty-container")&&l(this).attr("data-emptyString",t)})},_pb_edit:function(e){var o=l(".metabox-holder").get(0);o&&(o.className=o.className.replace(/columns-\d+/,"columns-"+e)),l(document).trigger("postboxes-columnchange")},_pb_change:function(){var e=l('label.columns-prefs-1 input[type="radio"]');switch(window.orientation){case 90:case-90:e.length&&e.is(":checked")||this._pb_edit(2);break;case 0:case 180:l("#poststuff").length?this._pb_edit(1):e.length&&e.is(":checked")||this._pb_edit(2)}},pbshow:!1,pbhide:!1}}(jQuery);PK1YZ;AKccpassword-strength-meter.min.jsnu[/*! This file is auto-generated */ window.wp=window.wp||{},function(a){var e=wp.i18n.__,n=wp.i18n.sprintf;wp.passwordStrength={meter:function(e,n,t){return Array.isArray(n)||(n=[n.toString()]),e!=t&&t&&0