diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:56:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:56:49 +0000 |
commit | a415c29efee45520ae252d2aa28f1083a521cd7b (patch) | |
tree | f4ade4b6668ecc0765de7e1424f7c1427ad433ff /wp-admin/js | |
parent | Initial commit. (diff) | |
download | wordpress-a415c29efee45520ae252d2aa28f1083a521cd7b.tar.xz wordpress-a415c29efee45520ae252d2aa28f1083a521cd7b.zip |
Adding upstream version 6.4.3+dfsg1.upstream/6.4.3+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'wp-admin/js')
109 files changed, 45823 insertions, 0 deletions
diff --git a/wp-admin/js/accordion.js b/wp-admin/js/accordion.js new file mode 100644 index 0000000..c420e8c --- /dev/null +++ b/wp-admin/js/accordion.js @@ -0,0 +1,94 @@ +/** + * 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: + * + * <div class="accordion-container"> + * <div class="accordion-section open"> + * <h3 class="accordion-section-title"></h3> + * <div class="accordion-section-content"> + * </div> + * </div> + * <div class="accordion-section"> + * <h3 class="accordion-section-title"></h3> + * <div class="accordion-section-content"> + * </div> + * </div> + * <div class="accordion-section"> + * <h3 class="accordion-section-title"></h3> + * <div class="accordion-section-content"> + * </div> + * </div> + * </div> + * + * 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 keydown', '.accordion-section-title', function( e ) { + if ( e.type === 'keydown' && 13 !== e.which ) { // "Return" key. + return; + } + + e.preventDefault(); // Keep this AFTER the key filter above. + + 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' ), + sectionToggleControl = section.find( '[aria-expanded]' ).first(), + 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 ( sectionToggleControl ) { + sectionToggleControl.attr( 'aria-expanded', String( sectionToggleControl.attr( 'aria-expanded' ) === 'false' ) ); + } + } + +})(jQuery); diff --git a/wp-admin/js/accordion.min.js b/wp-admin/js/accordion.min.js new file mode 100644 index 0000000..d54a50e --- /dev/null +++ b/wp-admin/js/accordion.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(s){s(function(){s(".accordion-container").on("click keydown",".accordion-section-title",function(e){var n,o,a,i,t;"keydown"===e.type&&13!==e.which||(e.preventDefault(),e=(e=s(this)).closest(".accordion-section"),n=e.find("[aria-expanded]").first(),o=e.closest(".accordion-container"),a=o.find(".open"),i=a.find("[aria-expanded]").first(),t=e.find(".accordion-section-content"),e.hasClass("cannot-expand"))||(o.addClass("opening"),e.hasClass("open")?(e.toggleClass("open"),t.toggle(!0).slideToggle(150)):(i.attr("aria-expanded","false"),a.removeClass("open"),a.find(".accordion-section-content").show().slideUp(150),t.toggle(!1).slideToggle(150),e.toggleClass("open")),setTimeout(function(){o.removeClass("opening")},150),n&&n.attr("aria-expanded",String("false"===n.attr("aria-expanded"))))})})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/application-passwords.js b/wp-admin/js/application-passwords.js new file mode 100644 index 0000000..c79cdb8 --- /dev/null +++ b/wp-admin/js/application-passwords.js @@ -0,0 +1,219 @@ +/** + * @output wp-admin/js/application-passwords.js + */ + +( function( $ ) { + var $appPassSection = $( '#application-passwords-section' ), + $newAppPassForm = $appPassSection.find( '.create-application-password' ), + $newAppPassField = $newAppPassForm.find( '.input' ), + $newAppPassButton = $newAppPassForm.find( '.button' ), + $appPassTwrapper = $appPassSection.find( '.application-passwords-list-table-wrapper' ), + $appPassTbody = $appPassSection.find( 'tbody' ), + $appPassTrNoItems = $appPassTbody.find( '.no-items' ), + $removeAllBtn = $( '#revoke-all-application-passwords' ), + tmplNewAppPass = wp.template( 'new-application-password' ), + tmplAppPassRow = wp.template( 'application-password-row' ), + userId = $( '#user_id' ).val(); + + $newAppPassButton.on( 'click', function( e ) { + e.preventDefault(); + + if ( $newAppPassButton.prop( 'aria-disabled' ) ) { + return; + } + + var name = $newAppPassField.val(); + + if ( 0 === name.length ) { + $newAppPassField.trigger( 'focus' ); + return; + } + + clearNotices(); + $newAppPassButton.prop( 'aria-disabled', true ).addClass( 'disabled' ); + + var request = { + name: name + }; + + /** + * Filters the request data used to create a new Application Password. + * + * @since 5.6.0 + * + * @param {Object} request The request data. + * @param {number} userId The id of the user the password is added for. + */ + request = wp.hooks.applyFilters( 'wp_application_passwords_new_password_request', request, userId ); + + wp.apiRequest( { + path: '/wp/v2/users/' + userId + '/application-passwords?_locale=user', + method: 'POST', + data: request + } ).always( function() { + $newAppPassButton.removeProp( 'aria-disabled' ).removeClass( 'disabled' ); + } ).done( function( response ) { + $newAppPassField.val( '' ); + $newAppPassButton.prop( 'disabled', false ); + + $newAppPassForm.after( tmplNewAppPass( { + name: response.name, + password: response.password + } ) ); + $( '.new-application-password-notice' ).attr( 'tabindex', '-1' ).trigger( 'focus' ); + + $appPassTbody.prepend( tmplAppPassRow( response ) ); + + $appPassTwrapper.show(); + $appPassTrNoItems.remove(); + + /** + * Fires after an application password has been successfully created. + * + * @since 5.6.0 + * + * @param {Object} response The response data from the REST API. + * @param {Object} request The request data used to create the password. + */ + wp.hooks.doAction( 'wp_application_passwords_created_password', response, request ); + } ).fail( handleErrorResponse ); + } ); + + $appPassTbody.on( 'click', '.delete', function( e ) { + e.preventDefault(); + + if ( ! window.confirm( wp.i18n.__( 'Are you sure you want to revoke this password? This action cannot be undone.' ) ) ) { + return; + } + + var $submitButton = $( this ), + $tr = $submitButton.closest( 'tr' ), + uuid = $tr.data( 'uuid' ); + + clearNotices(); + $submitButton.prop( 'disabled', true ); + + wp.apiRequest( { + path: '/wp/v2/users/' + userId + '/application-passwords/' + uuid + '?_locale=user', + method: 'DELETE' + } ).always( function() { + $submitButton.prop( 'disabled', false ); + } ).done( function( response ) { + if ( response.deleted ) { + if ( 0 === $tr.siblings().length ) { + $appPassTwrapper.hide(); + } + $tr.remove(); + + addNotice( wp.i18n.__( 'Application password revoked.' ), 'success' ).trigger( 'focus' ); + } + } ).fail( handleErrorResponse ); + } ); + + $removeAllBtn.on( 'click', function( e ) { + e.preventDefault(); + + if ( ! window.confirm( wp.i18n.__( 'Are you sure you want to revoke all passwords? This action cannot be undone.' ) ) ) { + return; + } + + var $submitButton = $( this ); + + clearNotices(); + $submitButton.prop( 'disabled', true ); + + wp.apiRequest( { + path: '/wp/v2/users/' + userId + '/application-passwords?_locale=user', + method: 'DELETE' + } ).always( function() { + $submitButton.prop( 'disabled', false ); + } ).done( function( response ) { + if ( response.deleted ) { + $appPassTbody.children().remove(); + $appPassSection.children( '.new-application-password' ).remove(); + $appPassTwrapper.hide(); + + addNotice( wp.i18n.__( 'All application passwords revoked.' ), 'success' ).trigger( 'focus' ); + } + } ).fail( handleErrorResponse ); + } ); + + $appPassSection.on( 'click', '.notice-dismiss', function( e ) { + e.preventDefault(); + var $el = $( this ).parent(); + $el.removeAttr( 'role' ); + $el.fadeTo( 100, 0, function () { + $el.slideUp( 100, function () { + $el.remove(); + $newAppPassField.trigger( 'focus' ); + } ); + } ); + } ); + + $newAppPassField.on( 'keypress', function ( e ) { + if ( 13 === e.which ) { + e.preventDefault(); + $newAppPassButton.trigger( 'click' ); + } + } ); + + // If there are no items, don't display the table yet. If there are, show it. + if ( 0 === $appPassTbody.children( 'tr' ).not( $appPassTrNoItems ).length ) { + $appPassTwrapper.hide(); + } + + /** + * Handles an error response from the REST API. + * + * @since 5.6.0 + * + * @param {jqXHR} xhr The XHR object from the ajax call. + * @param {string} textStatus The string categorizing the ajax request's status. + * @param {string} errorThrown The HTTP status error text. + */ + function handleErrorResponse( xhr, textStatus, errorThrown ) { + var errorMessage = errorThrown; + + if ( xhr.responseJSON && xhr.responseJSON.message ) { + errorMessage = xhr.responseJSON.message; + } + + addNotice( errorMessage, 'error' ); + } + + /** + * Displays a message in the Application Passwords section. + * + * @since 5.6.0 + * + * @param {string} message The message to display. + * @param {string} type The notice type. Either 'success' or 'error'. + * @returns {jQuery} The notice element. + */ + function addNotice( message, type ) { + var $notice = $( '<div></div>' ) + .attr( 'role', 'alert' ) + .attr( 'tabindex', '-1' ) + .addClass( 'is-dismissible notice notice-' + type ) + .append( $( '<p></p>' ).text( message ) ) + .append( + $( '<button></button>' ) + .attr( 'type', 'button' ) + .addClass( 'notice-dismiss' ) + .append( $( '<span></span>' ).addClass( 'screen-reader-text' ).text( wp.i18n.__( 'Dismiss this notice.' ) ) ) + ); + + $newAppPassForm.after( $notice ); + + return $notice; + } + + /** + * Clears notice messages from the Application Passwords section. + * + * @since 5.6.0 + */ + function clearNotices() { + $( '.notice', $appPassSection ).remove(); + } +}( jQuery ) ); diff --git a/wp-admin/js/application-passwords.min.js b/wp-admin/js/application-passwords.min.js new file mode 100644 index 0000000..12d7cf7 --- /dev/null +++ b/wp-admin/js/application-passwords.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(o){var a=o("#application-passwords-section"),i=a.find(".create-application-password"),t=i.find(".input"),n=i.find(".button"),p=a.find(".application-passwords-list-table-wrapper"),r=a.find("tbody"),d=r.find(".no-items"),e=o("#revoke-all-application-passwords"),l=wp.template("new-application-password"),c=wp.template("application-password-row"),u=o("#user_id").val();function w(e,s,a){f(a=e.responseJSON&&e.responseJSON.message?e.responseJSON.message:a,"error")}function f(e,s){s=o("<div></div>").attr("role","alert").attr("tabindex","-1").addClass("is-dismissible notice notice-"+s).append(o("<p></p>").text(e)).append(o("<button></button>").attr("type","button").addClass("notice-dismiss").append(o("<span></span>").addClass("screen-reader-text").text(wp.i18n.__("Dismiss this notice."))));return i.after(s),s}function v(){o(".notice",a).remove()}n.on("click",function(e){var s;e.preventDefault(),n.prop("aria-disabled")||(0===(e=t.val()).length?t.trigger("focus"):(v(),n.prop("aria-disabled",!0).addClass("disabled"),s={name:e},s=wp.hooks.applyFilters("wp_application_passwords_new_password_request",s,u),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords?_locale=user",method:"POST",data:s}).always(function(){n.removeProp("aria-disabled").removeClass("disabled")}).done(function(e){t.val(""),n.prop("disabled",!1),i.after(l({name:e.name,password:e.password})),o(".new-application-password-notice").attr("tabindex","-1").trigger("focus"),r.prepend(c(e)),p.show(),d.remove(),wp.hooks.doAction("wp_application_passwords_created_password",e,s)}).fail(w)))}),r.on("click",".delete",function(e){var s,a;e.preventDefault(),window.confirm(wp.i18n.__("Are you sure you want to revoke this password? This action cannot be undone."))&&(s=o(this),e=(a=s.closest("tr")).data("uuid"),v(),s.prop("disabled",!0),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords/"+e+"?_locale=user",method:"DELETE"}).always(function(){s.prop("disabled",!1)}).done(function(e){e.deleted&&(0===a.siblings().length&&p.hide(),a.remove(),f(wp.i18n.__("Application password revoked."),"success").trigger("focus"))}).fail(w))}),e.on("click",function(e){var s;e.preventDefault(),window.confirm(wp.i18n.__("Are you sure you want to revoke all passwords? This action cannot be undone."))&&(s=o(this),v(),s.prop("disabled",!0),wp.apiRequest({path:"/wp/v2/users/"+u+"/application-passwords?_locale=user",method:"DELETE"}).always(function(){s.prop("disabled",!1)}).done(function(e){e.deleted&&(r.children().remove(),a.children(".new-application-password").remove(),p.hide(),f(wp.i18n.__("All application passwords revoked."),"success").trigger("focus"))}).fail(w))}),a.on("click",".notice-dismiss",function(e){e.preventDefault();var s=o(this).parent();s.removeAttr("role"),s.fadeTo(100,0,function(){s.slideUp(100,function(){s.remove(),t.trigger("focus")})})}),t.on("keypress",function(e){13===e.which&&(e.preventDefault(),n.trigger("click"))}),0===r.children("tr").not(d).length&&p.hide()}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/auth-app.js b/wp-admin/js/auth-app.js new file mode 100644 index 0000000..99478d1 --- /dev/null +++ b/wp-admin/js/auth-app.js @@ -0,0 +1,165 @@ +/** + * @output wp-admin/js/auth-app.js + */ + +/* global authApp */ + +( function( $, authApp ) { + var $appNameField = $( '#app_name' ), + $approveBtn = $( '#approve' ), + $rejectBtn = $( '#reject' ), + $form = $appNameField.closest( 'form' ), + context = { + userLogin: authApp.user_login, + successUrl: authApp.success, + rejectUrl: authApp.reject + }; + + $approveBtn.on( 'click', function( e ) { + var name = $appNameField.val(), + appId = $( 'input[name="app_id"]', $form ).val(); + + e.preventDefault(); + + if ( $approveBtn.prop( 'aria-disabled' ) ) { + return; + } + + if ( 0 === name.length ) { + $appNameField.trigger( 'focus' ); + return; + } + + $approveBtn.prop( 'aria-disabled', true ).addClass( 'disabled' ); + + var request = { + name: name + }; + + if ( appId.length > 0 ) { + request.app_id = appId; + } + + /** + * Filters the request data used to Authorize an Application Password request. + * + * @since 5.6.0 + * + * @param {Object} request The request data. + * @param {Object} context Context about the Application Password request. + * @param {string} context.userLogin The user's login username. + * @param {string} context.successUrl The URL the user will be redirected to after approving the request. + * @param {string} context.rejectUrl The URL the user will be redirected to after rejecting the request. + */ + request = wp.hooks.applyFilters( 'wp_application_passwords_approve_app_request', request, context ); + + wp.apiRequest( { + path: '/wp/v2/users/me/application-passwords?_locale=user', + method: 'POST', + data: request + } ).done( function( response, textStatus, jqXHR ) { + + /** + * Fires when an Authorize Application Password request has been successfully approved. + * + * In most cases, this should be used in combination with the {@see 'wp_authorize_application_password_form_approved_no_js'} + * action to ensure that both the JS and no-JS variants are handled. + * + * @since 5.6.0 + * + * @param {Object} response The response from the REST API. + * @param {string} response.password The newly created password. + * @param {string} textStatus The status of the request. + * @param {jqXHR} jqXHR The underlying jqXHR object that made the request. + */ + wp.hooks.doAction( 'wp_application_passwords_approve_app_request_success', response, textStatus, jqXHR ); + + var raw = authApp.success, + url, message, $notice; + + if ( raw ) { + url = raw + ( -1 === raw.indexOf( '?' ) ? '?' : '&' ) + + 'site_url=' + encodeURIComponent( authApp.site_url ) + + '&user_login=' + encodeURIComponent( authApp.user_login ) + + '&password=' + encodeURIComponent( response.password ); + + window.location = url; + } else { + message = wp.i18n.sprintf( + /* translators: %s: Application name. */ + '<label for="new-application-password-value">' + wp.i18n.__( 'Your new password for %s is:' ) + '</label>', + '<strong></strong>' + ) + ' <input id="new-application-password-value" type="text" class="code" readonly="readonly" value="" />'; + $notice = $( '<div></div>' ) + .attr( 'role', 'alert' ) + .attr( 'tabindex', -1 ) + .addClass( 'notice notice-success notice-alt' ) + .append( $( '<p></p>' ).addClass( 'application-password-display' ).html( message ) ) + .append( '<p>' + wp.i18n.__( 'Be sure to save this in a safe location. You will not be able to retrieve it.' ) + '</p>' ); + + // We're using .text() to write the variables to avoid any chance of XSS. + $( 'strong', $notice ).text( response.name ); + $( 'input', $notice ).val( response.password ); + + $form.replaceWith( $notice ); + $notice.trigger( 'focus' ); + } + } ).fail( function( jqXHR, textStatus, errorThrown ) { + var errorMessage = errorThrown, + error = null; + + if ( jqXHR.responseJSON ) { + error = jqXHR.responseJSON; + + if ( error.message ) { + errorMessage = error.message; + } + } + + var $notice = $( '<div></div>' ) + .attr( 'role', 'alert' ) + .addClass( 'notice notice-error' ) + .append( $( '<p></p>' ).text( errorMessage ) ); + + $( 'h1' ).after( $notice ); + + $approveBtn.removeProp( 'aria-disabled', false ).removeClass( 'disabled' ); + + /** + * Fires when an Authorize Application Password request encountered an error when trying to approve the request. + * + * @since 5.6.0 + * @since 5.6.1 Corrected action name and signature. + * + * @param {Object|null} error The error from the REST API. May be null if the server did not send proper JSON. + * @param {string} textStatus The status of the request. + * @param {string} errorThrown The error message associated with the response status code. + * @param {jqXHR} jqXHR The underlying jqXHR object that made the request. + */ + wp.hooks.doAction( 'wp_application_passwords_approve_app_request_error', error, textStatus, errorThrown, jqXHR ); + } ); + } ); + + $rejectBtn.on( 'click', function( e ) { + e.preventDefault(); + + /** + * Fires when an Authorize Application Password request has been rejected by the user. + * + * @since 5.6.0 + * + * @param {Object} context Context about the Application Password request. + * @param {string} context.userLogin The user's login username. + * @param {string} context.successUrl The URL the user will be redirected to after approving the request. + * @param {string} context.rejectUrl The URL the user will be redirected to after rejecting the request. + */ + wp.hooks.doAction( 'wp_application_passwords_reject_app', context ); + + // @todo: Make a better way to do this so it feels like less of a semi-open redirect. + window.location = authApp.reject; + } ); + + $form.on( 'submit', function( e ) { + e.preventDefault(); + } ); +}( jQuery, authApp ) ); diff --git a/wp-admin/js/auth-app.min.js b/wp-admin/js/auth-app.min.js new file mode 100644 index 0000000..110fcdf --- /dev/null +++ b/wp-admin/js/auth-app.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(t,s){var p=t("#app_name"),r=t("#approve"),e=t("#reject"),n=p.closest("form"),i={userLogin:s.user_login,successUrl:s.success,rejectUrl:s.reject};r.on("click",function(e){var a=p.val(),o=t('input[name="app_id"]',n).val();e.preventDefault(),r.prop("aria-disabled")||(0===a.length?p.trigger("focus"):(r.prop("aria-disabled",!0).addClass("disabled"),e={name:a},0<o.length&&(e.app_id=o),e=wp.hooks.applyFilters("wp_application_passwords_approve_app_request",e,i),wp.apiRequest({path:"/wp/v2/users/me/application-passwords?_locale=user",method:"POST",data:e}).done(function(e,a,o){wp.hooks.doAction("wp_application_passwords_approve_app_request_success",e,a,o);var a=s.success;a?(o=a+(-1===a.indexOf("?")?"?":"&")+"site_url="+encodeURIComponent(s.site_url)+"&user_login="+encodeURIComponent(s.user_login)+"&password="+encodeURIComponent(e.password),window.location=o):(a=wp.i18n.sprintf('<label for="new-application-password-value">'+wp.i18n.__("Your new password for %s is:")+"</label>","<strong></strong>")+' <input id="new-application-password-value" type="text" class="code" readonly="readonly" value="" />',o=t("<div></div>").attr("role","alert").attr("tabindex",-1).addClass("notice notice-success notice-alt").append(t("<p></p>").addClass("application-password-display").html(a)).append("<p>"+wp.i18n.__("Be sure to save this in a safe location. You will not be able to retrieve it.")+"</p>"),t("strong",o).text(e.name),t("input",o).val(e.password),n.replaceWith(o),o.trigger("focus"))}).fail(function(e,a,o){var s=o,p=null,s=(e.responseJSON&&(p=e.responseJSON).message&&(s=p.message),t("<div></div>").attr("role","alert").addClass("notice notice-error").append(t("<p></p>").text(s)));t("h1").after(s),r.removeProp("aria-disabled",!1).removeClass("disabled"),wp.hooks.doAction("wp_application_passwords_approve_app_request_error",p,a,o,e)})))}),e.on("click",function(e){e.preventDefault(),wp.hooks.doAction("wp_application_passwords_reject_app",i),window.location=s.reject}),n.on("submit",function(e){e.preventDefault()})}(jQuery,authApp);
\ No newline at end of file diff --git a/wp-admin/js/code-editor.js b/wp-admin/js/code-editor.js new file mode 100644 index 0000000..68365d7 --- /dev/null +++ b/wp-admin/js/code-editor.js @@ -0,0 +1,346 @@ +/** + * @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 <https://github.com/codemirror/CodeMirror/pull/4944>. + */ + 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 ); diff --git a/wp-admin/js/code-editor.min.js b/wp-admin/js/code-editor.min.js new file mode 100644 index 0000000..1e35ef5 --- /dev/null +++ b/wp-admin/js/code-editor.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +void 0===window.wp&&(window.wp={}),void 0===window.wp.codeEditor&&(window.wp.codeEditor={}),function(u,d){"use strict";function s(r,s){var a=[],d=[];function c(){s.onUpdateErrorNotice&&!_.isEqual(a,d)&&(s.onUpdateErrorNotice(a,r),d=a)}function i(){var i,t=r.getOption("lint");return!!t&&(!0===t?t={}:_.isObject(t)&&(t=u.extend({},t)),t.options||(t.options={}),"javascript"===s.codemirror.mode&&s.jshint&&u.extend(t.options,s.jshint),"css"===s.codemirror.mode&&s.csslint&&u.extend(t.options,s.csslint),"htmlmixed"===s.codemirror.mode&&s.htmlhint&&(t.options.rules=u.extend({},s.htmlhint),s.jshint&&(t.options.rules.jshint=s.jshint),s.csslint)&&(t.options.rules.csslint=s.csslint),t.onUpdateLinting=(i=t.onUpdateLinting,function(t,e,n){var o=_.filter(t,function(t){return"error"===t.severity});i&&i.apply(t,e,n),!_.isEqual(o,a)&&(a=o,s.onChangeLintingErrors&&s.onChangeLintingErrors(o,t,e,n),!r.state.focused||0===a.length||0<d.length)&&c()}),t)}r.setOption("lint",i()),r.on("optionChange",function(t,e){var n,o="CodeMirror-lint-markers";"lint"===e&&(e=r.getOption("gutters")||[],!0===(n=r.getOption("lint"))?(_.contains(e,o)||r.setOption("gutters",[o].concat(e)),r.setOption("lint",i())):n||r.setOption("gutters",_.without(e,o)),r.getOption("lint")?r.performLint():(a=[],c()))}),r.on("blur",c),r.on("startCompletion",function(){r.off("blur",c)}),r.on("endCompletion",function(){r.on("blur",c),_.delay(function(){r.state.focused||c()},500)}),u(document.body).on("mousedown",function(t){!r.state.focused||u.contains(r.display.wrapper,t.target)||u(t.target).hasClass("CodeMirror-hint")||c()})}d.codeEditor.defaultSettings={codemirror:{},csslint:{},htmlhint:{},jshint:{},onTabNext:function(){},onTabPrevious:function(){},onChangeLintingErrors:function(){},onUpdateErrorNotice:function(){}},d.codeEditor.initialize=function(t,e){var a,n,o,i,t=u("string"==typeof t?"#"+t:t),r=u.extend({},d.codeEditor.defaultSettings,e);return r.codemirror=u.extend({},r.codemirror),s(a=d.CodeMirror.fromTextArea(t[0],r.codemirror),r),t={settings:r,codemirror:a},a.showHint&&a.on("keyup",function(t,e){var n,o,i,r,s=/^[a-zA-Z]$/.test(e.key);a.state.completionActive&&s||"string"!==(r=a.getTokenAt(a.getCursor())).type&&"comment"!==r.type&&(i=d.CodeMirror.innerMode(a.getMode(),r.state).mode.name,o=a.doc.getLine(a.doc.getCursor().line).substr(0,a.doc.getCursor().ch),"html"===i||"xml"===i?n="<"===e.key||"/"===e.key&&"tag"===r.type||s&&"tag"===r.type||s&&"attribute"===r.type||"="===r.string&&r.state.htmlState&&r.state.htmlState.tagName:"css"===i?n=s||":"===e.key||" "===e.key&&/:\s+$/.test(o):"javascript"===i?n=s||"."===e.key:"clike"===i&&"php"===a.options.mode&&(n="keyword"===r.type||"variable"===r.type),n)&&a.showHint({completeSingle:!1})}),o=e,i=u((n=a).getTextArea()),n.on("blur",function(){i.data("next-tab-blurs",!1)}),n.on("keydown",function(t,e){27===e.keyCode?i.data("next-tab-blurs",!0):9===e.keyCode&&i.data("next-tab-blurs")&&(e.shiftKey?o.onTabPrevious(n,e):o.onTabNext(n,e),i.data("next-tab-blurs",!1),e.preventDefault())}),t}}(window.jQuery,window.wp);
\ No newline at end of file diff --git a/wp-admin/js/color-picker.js b/wp-admin/js/color-picker.js new file mode 100644 index 0000000..600e023 --- /dev/null +++ b/wp-admin/js/color-picker.js @@ -0,0 +1,356 @@ +/** + * @output wp-admin/js/color-picker.js + */ + +( function( $, undef ) { + + var ColorPicker, + _before = '<button type="button" class="button wp-color-result" aria-expanded="false"><span class="wp-color-result-text"></span></button>', + _after = '<div class="wp-picker-holder" />', + _wrap = '<div class="wp-picker-container" />', + _button = '<input type="button" class="button button-small" />', + _wrappingLabel = '<label></label>', + _wrappingLabelText = '<span class="screen-reader-text"></span>', + __ = wp.i18n.__; + + /** + * Creates a jQuery UI color picker that is used in the theme customizer. + * + * @class $.widget.wp.wpColorPicker + * + * @since 3.5.0 + */ + ColorPicker = /** @lends $.widget.wp.wpColorPicker.prototype */{ + options: { + defaultColor: false, + change: false, + clear: false, + hide: true, + palettes: true, + width: 255, + mode: 'hsv', + type: 'full', + slider: 'horizontal' + }, + /** + * Creates a color picker that only allows you to adjust the hue. + * + * @since 3.5.0 + * @access private + * + * @return {void} + */ + _createHueOnly: function() { + var self = this, + el = self.element, + color; + + el.hide(); + + // Set the saturation to the maximum level. + color = 'hsl(' + el.val() + ', 100, 50)'; + + // Create an instance of the color picker, using the hsl mode. + el.iris( { + mode: 'hsl', + type: 'hue', + hide: false, + color: color, + /** + * Handles the onChange event if one has been defined in the options. + * + * @ignore + * + * @param {Event} event The event that's being called. + * @param {HTMLElement} ui The HTMLElement containing the color picker. + * + * @return {void} + */ + change: function( event, ui ) { + if ( typeof self.options.change === 'function' ) { + self.options.change.call( this, event, ui ); + } + }, + width: self.options.width, + slider: self.options.slider + } ); + }, + /** + * Creates the color picker, sets default values, css classes and wraps it all in HTML. + * + * @since 3.5.0 + * @access private + * + * @return {void} + */ + _create: function() { + // Return early if Iris support is missing. + if ( ! $.support.iris ) { + return; + } + + var self = this, + el = self.element; + + // Override default options with options bound to the element. + $.extend( self.options, el.data() ); + + // Create a color picker which only allows adjustments to the hue. + if ( self.options.type === 'hue' ) { + return self._createHueOnly(); + } + + // Bind the close event. + self.close = self.close.bind( self ); + + self.initialValue = el.val(); + + // Add a CSS class to the input field. + el.addClass( 'wp-color-picker' ); + + /* + * Check if there's already a wrapping label, e.g. in the Customizer. + * If there's no label, add a default one to match the Customizer template. + */ + if ( ! el.parent( 'label' ).length ) { + // Wrap the input field in the default label. + el.wrap( _wrappingLabel ); + // Insert the default label text. + self.wrappingLabelText = $( _wrappingLabelText ) + .insertBefore( el ) + .text( __( 'Color value' ) ); + } + + /* + * At this point, either it's the standalone version or the Customizer + * one, we have a wrapping label to use as hook in the DOM, let's store it. + */ + self.wrappingLabel = el.parent(); + + // Wrap the label in the main wrapper. + self.wrappingLabel.wrap( _wrap ); + // Store a reference to the main wrapper. + self.wrap = self.wrappingLabel.parent(); + // Set up the toggle button and insert it before the wrapping label. + self.toggler = $( _before ) + .insertBefore( self.wrappingLabel ) + .css( { backgroundColor: self.initialValue } ); + // Set the toggle button span element text. + self.toggler.find( '.wp-color-result-text' ).text( __( 'Select Color' ) ); + // Set up the Iris container and insert it after the wrapping label. + self.pickerContainer = $( _after ).insertAfter( self.wrappingLabel ); + // Store a reference to the Clear/Default button. + self.button = $( _button ); + + // Set up the Clear/Default button. + if ( self.options.defaultColor ) { + self.button + .addClass( 'wp-picker-default' ) + .val( __( 'Default' ) ) + .attr( 'aria-label', __( 'Select default color' ) ); + } else { + self.button + .addClass( 'wp-picker-clear' ) + .val( __( 'Clear' ) ) + .attr( 'aria-label', __( 'Clear color' ) ); + } + + // Wrap the wrapping label in its wrapper and append the Clear/Default button. + self.wrappingLabel + .wrap( '<span class="wp-picker-input-wrap hidden" />' ) + .after( self.button ); + + /* + * The input wrapper now contains the label+input+Clear/Default button. + * Store a reference to the input wrapper: we'll use this to toggle + * the controls visibility. + */ + self.inputWrapper = el.closest( '.wp-picker-input-wrap' ); + + el.iris( { + target: self.pickerContainer, + hide: self.options.hide, + width: self.options.width, + mode: self.options.mode, + palettes: self.options.palettes, + /** + * Handles the onChange event if one has been defined in the options and additionally + * sets the background color for the toggler element. + * + * @since 3.5.0 + * + * @ignore + * + * @param {Event} event The event that's being called. + * @param {HTMLElement} ui The HTMLElement containing the color picker. + * + * @return {void} + */ + change: function( event, ui ) { + self.toggler.css( { backgroundColor: ui.color.toString() } ); + + if ( typeof self.options.change === 'function' ) { + self.options.change.call( this, event, ui ); + } + } + } ); + + el.val( self.initialValue ); + self._addListeners(); + + // Force the color picker to always be closed on initial load. + if ( ! self.options.hide ) { + self.toggler.click(); + } + }, + /** + * Binds event listeners to the color picker. + * + * @since 3.5.0 + * @access private + * + * @return {void} + */ + _addListeners: function() { + var self = this; + + /** + * Prevent any clicks inside this widget from leaking to the top and closing it. + * + * @since 3.5.0 + * + * @param {Event} event The event that's being called. + * + * @return {void} + */ + self.wrap.on( 'click.wpcolorpicker', function( event ) { + event.stopPropagation(); + }); + + /** + * Open or close the color picker depending on the class. + * + * @since 3.5.0 + */ + self.toggler.on( 'click', function(){ + if ( self.toggler.hasClass( 'wp-picker-open' ) ) { + self.close(); + } else { + self.open(); + } + }); + + /** + * Checks if value is empty when changing the color in the color picker. + * If so, the background color is cleared. + * + * @since 3.5.0 + * + * @param {Event} event The event that's being called. + * + * @return {void} + */ + self.element.on( 'change', function( event ) { + var me = $( this ), + val = me.val(); + + if ( val === '' || val === '#' ) { + self.toggler.css( 'backgroundColor', '' ); + // Fire clear callback if we have one. + if ( typeof self.options.clear === 'function' ) { + self.options.clear.call( this, event ); + } + } + }); + + /** + * Enables the user to either clear the color in the color picker or revert back to the default color. + * + * @since 3.5.0 + * + * @param {Event} event The event that's being called. + * + * @return {void} + */ + self.button.on( 'click', function( event ) { + var me = $( this ); + if ( me.hasClass( 'wp-picker-clear' ) ) { + self.element.val( '' ); + self.toggler.css( 'backgroundColor', '' ); + if ( typeof self.options.clear === 'function' ) { + self.options.clear.call( this, event ); + } + } else if ( me.hasClass( 'wp-picker-default' ) ) { + self.element.val( self.options.defaultColor ).change(); + } + }); + }, + /** + * Opens the color picker dialog. + * + * @since 3.5.0 + * + * @return {void} + */ + open: function() { + this.element.iris( 'toggle' ); + this.inputWrapper.removeClass( 'hidden' ); + this.wrap.addClass( 'wp-picker-active' ); + this.toggler + .addClass( 'wp-picker-open' ) + .attr( 'aria-expanded', 'true' ); + $( 'body' ).trigger( 'click.wpcolorpicker' ).on( 'click.wpcolorpicker', this.close ); + }, + /** + * Closes the color picker dialog. + * + * @since 3.5.0 + * + * @return {void} + */ + close: function() { + this.element.iris( 'toggle' ); + this.inputWrapper.addClass( 'hidden' ); + this.wrap.removeClass( 'wp-picker-active' ); + this.toggler + .removeClass( 'wp-picker-open' ) + .attr( 'aria-expanded', 'false' ); + $( 'body' ).off( 'click.wpcolorpicker', this.close ); + }, + /** + * Returns the iris object if no new color is provided. If a new color is provided, it sets the new color. + * + * @param newColor {string|*} The new color to use. Can be undefined. + * + * @since 3.5.0 + * + * @return {string} The element's color. + */ + color: function( newColor ) { + if ( newColor === undef ) { + return this.element.iris( 'option', 'color' ); + } + this.element.iris( 'option', 'color', newColor ); + }, + /** + * Returns the iris object if no new default color is provided. + * If a new default color is provided, it sets the new default color. + * + * @param newDefaultColor {string|*} The new default color to use. Can be undefined. + * + * @since 3.5.0 + * + * @return {boolean|string} The element's color. + */ + defaultColor: function( newDefaultColor ) { + if ( newDefaultColor === undef ) { + return this.options.defaultColor; + } + + this.options.defaultColor = newDefaultColor; + } + }; + + // Register the color picker as a widget. + $.widget( 'wp.wpColorPicker', ColorPicker ); +}( jQuery ) ); diff --git a/wp-admin/js/color-picker.min.js b/wp-admin/js/color-picker.min.js new file mode 100644 index 0000000..7718bbd --- /dev/null +++ b/wp-admin/js/color-picker.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(i,t){var a=wp.i18n.__;i.widget("wp.wpColorPicker",{options:{defaultColor:!1,change:!1,clear:!1,hide:!0,palettes:!0,width:255,mode:"hsv",type:"full",slider:"horizontal"},_createHueOnly:function(){var e,o=this,t=o.element;t.hide(),e="hsl("+t.val()+", 100, 50)",t.iris({mode:"hsl",type:"hue",hide:!1,color:e,change:function(e,t){"function"==typeof o.options.change&&o.options.change.call(this,e,t)},width:o.options.width,slider:o.options.slider})},_create:function(){if(i.support.iris){var o=this,e=o.element;if(i.extend(o.options,e.data()),"hue"===o.options.type)return o._createHueOnly();o.close=o.close.bind(o),o.initialValue=e.val(),e.addClass("wp-color-picker"),e.parent("label").length||(e.wrap("<label></label>"),o.wrappingLabelText=i('<span class="screen-reader-text"></span>').insertBefore(e).text(a("Color value"))),o.wrappingLabel=e.parent(),o.wrappingLabel.wrap('<div class="wp-picker-container" />'),o.wrap=o.wrappingLabel.parent(),o.toggler=i('<button type="button" class="button wp-color-result" aria-expanded="false"><span class="wp-color-result-text"></span></button>').insertBefore(o.wrappingLabel).css({backgroundColor:o.initialValue}),o.toggler.find(".wp-color-result-text").text(a("Select Color")),o.pickerContainer=i('<div class="wp-picker-holder" />').insertAfter(o.wrappingLabel),o.button=i('<input type="button" class="button button-small" />'),o.options.defaultColor?o.button.addClass("wp-picker-default").val(a("Default")).attr("aria-label",a("Select default color")):o.button.addClass("wp-picker-clear").val(a("Clear")).attr("aria-label",a("Clear color")),o.wrappingLabel.wrap('<span class="wp-picker-input-wrap hidden" />').after(o.button),o.inputWrapper=e.closest(".wp-picker-input-wrap"),e.iris({target:o.pickerContainer,hide:o.options.hide,width:o.options.width,mode:o.options.mode,palettes:o.options.palettes,change:function(e,t){o.toggler.css({backgroundColor:t.color.toString()}),"function"==typeof o.options.change&&o.options.change.call(this,e,t)}}),e.val(o.initialValue),o._addListeners(),o.options.hide||o.toggler.click()}},_addListeners:function(){var o=this;o.wrap.on("click.wpcolorpicker",function(e){e.stopPropagation()}),o.toggler.on("click",function(){o.toggler.hasClass("wp-picker-open")?o.close():o.open()}),o.element.on("change",function(e){var t=i(this).val();""!==t&&"#"!==t||(o.toggler.css("backgroundColor",""),"function"==typeof o.options.clear&&o.options.clear.call(this,e))}),o.button.on("click",function(e){var t=i(this);t.hasClass("wp-picker-clear")?(o.element.val(""),o.toggler.css("backgroundColor",""),"function"==typeof o.options.clear&&o.options.clear.call(this,e)):t.hasClass("wp-picker-default")&&o.element.val(o.options.defaultColor).change()})},open:function(){this.element.iris("toggle"),this.inputWrapper.removeClass("hidden"),this.wrap.addClass("wp-picker-active"),this.toggler.addClass("wp-picker-open").attr("aria-expanded","true"),i("body").trigger("click.wpcolorpicker").on("click.wpcolorpicker",this.close)},close:function(){this.element.iris("toggle"),this.inputWrapper.addClass("hidden"),this.wrap.removeClass("wp-picker-active"),this.toggler.removeClass("wp-picker-open").attr("aria-expanded","false"),i("body").off("click.wpcolorpicker",this.close)},color:function(e){if(e===t)return this.element.iris("option","color");this.element.iris("option","color",e)},defaultColor:function(e){if(e===t)return this.options.defaultColor;this.options.defaultColor=e}})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/comment.js b/wp-admin/js/comment.js new file mode 100644 index 0000000..4e4f3c5 --- /dev/null +++ b/wp-admin/js/comment.js @@ -0,0 +1,102 @@ +/** + * @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 - 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:' ) + ' <b>' + + /* 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 ) ) + + '</b> ' + ); + + // Move focus back to the Edit link. + $edittimestamp.show().trigger( 'focus' ); + $timestampdiv.slideUp( 'fast' ); + }); +}); diff --git a/wp-admin/js/comment.min.js b/wp-admin/js/comment.min.js new file mode 100644 index 0000000..9ce3d6b --- /dev/null +++ b/wp-admin/js/comment.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(m){postboxes.add_postbox_toggles("comment");var d=m("#timestampdiv"),o=m("#timestamp"),a=o.html(),v=d.find(".timestamp-wrap"),c=d.siblings("a.edit-timestamp");c.on("click",function(e){d.is(":hidden")&&(d.slideDown("fast",function(){m("input, select",v).first().trigger("focus")}),m(this).hide()),e.preventDefault()}),d.find(".cancel-timestamp").on("click",function(e){c.show().trigger("focus"),d.slideUp("fast"),m("#mm").val(m("#hidden_mm").val()),m("#jj").val(m("#hidden_jj").val()),m("#aa").val(m("#hidden_aa").val()),m("#hh").val(m("#hidden_hh").val()),m("#mn").val(m("#hidden_mn").val()),o.html(a),e.preventDefault()}),d.find(".save-timestamp").on("click",function(e){var a=m("#aa").val(),t=m("#mm").val(),i=m("#jj").val(),s=m("#hh").val(),l=m("#mn").val(),n=new Date(a,t-1,i,s,l);e.preventDefault(),n.getFullYear()!=a||1+n.getMonth()!=t||n.getDate()!=i||n.getMinutes()!=l?v.addClass("form-invalid"):(v.removeClass("form-invalid"),o.html(wp.i18n.__("Submitted on:")+" <b>"+wp.i18n.__("%1$s %2$s, %3$s at %4$s:%5$s").replace("%1$s",m('option[value="'+t+'"]',"#mm").attr("data-text")).replace("%2$s",parseInt(i,10)).replace("%3$s",a).replace("%4$s",("00"+s).slice(-2)).replace("%5$s",("00"+l).slice(-2))+"</b> "),c.show().trigger("focus"),d.slideUp("fast"))})});
\ No newline at end of file diff --git a/wp-admin/js/common.js b/wp-admin/js/common.js new file mode 100644 index 0000000..3de9447 --- /dev/null +++ b/wp-admin/js/common.js @@ -0,0 +1,2247 @@ +/** + * @output wp-admin/js/common.js + */ + +/* global setUserSetting, ajaxurl, alert, confirm, pagenow */ +/* global columns, screenMeta */ + +/** + * Adds common WordPress functionality to the window. + * + * @param {jQuery} $ jQuery object. + * @param {Object} window The window object. + * @param {mixed} undefined Unused. + */ +( function( $, window, undefined ) { + var $document = $( document ), + $window = $( window ), + $body = $( document.body ), + __ = wp.i18n.__, + sprintf = wp.i18n.sprintf; + +/** + * Throws an error for a deprecated property. + * + * @since 5.5.1 + * + * @param {string} propName The property that was used. + * @param {string} version The version of WordPress that deprecated the property. + * @param {string} replacement The property that should have been used. + */ +function deprecatedProperty( propName, version, replacement ) { + var message; + + if ( 'undefined' !== typeof replacement ) { + message = sprintf( + /* translators: 1: Deprecated property name, 2: Version number, 3: Alternative property name. */ + __( '%1$s is deprecated since version %2$s! Use %3$s instead.' ), + propName, + version, + replacement + ); + } else { + message = sprintf( + /* translators: 1: Deprecated property name, 2: Version number. */ + __( '%1$s is deprecated since version %2$s with no alternative available.' ), + propName, + version + ); + } + + window.console.warn( message ); +} + +/** + * Deprecate all properties on an object. + * + * @since 5.5.1 + * @since 5.6.0 Added the `version` parameter. + * + * @param {string} name The name of the object, i.e. commonL10n. + * @param {object} l10nObject The object to deprecate the properties on. + * @param {string} version The version of WordPress that deprecated the property. + * + * @return {object} The object with all its properties deprecated. + */ +function deprecateL10nObject( name, l10nObject, version ) { + var deprecatedObject = {}; + + Object.keys( l10nObject ).forEach( function( key ) { + var prop = l10nObject[ key ]; + var propName = name + '.' + key; + + if ( 'object' === typeof prop ) { + Object.defineProperty( deprecatedObject, key, { get: function() { + deprecatedProperty( propName, version, prop.alternative ); + return prop.func(); + } } ); + } else { + Object.defineProperty( deprecatedObject, key, { get: function() { + deprecatedProperty( propName, version, 'wp.i18n' ); + return prop; + } } ); + } + } ); + + return deprecatedObject; +} + +window.wp.deprecateL10nObject = deprecateL10nObject; + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.6.0 + * @deprecated 5.5.0 + */ +window.commonL10n = window.commonL10n || { + warnDelete: '', + dismiss: '', + collapseMenu: '', + expandMenu: '' +}; + +window.commonL10n = deprecateL10nObject( 'commonL10n', window.commonL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 3.3.0 + * @deprecated 5.5.0 + */ +window.wpPointerL10n = window.wpPointerL10n || { + dismiss: '' +}; + +window.wpPointerL10n = deprecateL10nObject( 'wpPointerL10n', window.wpPointerL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 4.3.0 + * @deprecated 5.5.0 + */ +window.userProfileL10n = window.userProfileL10n || { + warn: '', + warnWeak: '', + show: '', + hide: '', + cancel: '', + ariaShow: '', + ariaHide: '' +}; + +window.userProfileL10n = deprecateL10nObject( 'userProfileL10n', window.userProfileL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 4.9.6 + * @deprecated 5.5.0 + */ +window.privacyToolsL10n = window.privacyToolsL10n || { + noDataFound: '', + foundAndRemoved: '', + noneRemoved: '', + someNotRemoved: '', + removalError: '', + emailSent: '', + noExportFile: '', + exportError: '' +}; + +window.privacyToolsL10n = deprecateL10nObject( 'privacyToolsL10n', window.privacyToolsL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 3.6.0 + * @deprecated 5.5.0 + */ +window.authcheckL10n = { + beforeunload: '' +}; + +window.authcheckL10n = window.authcheckL10n || deprecateL10nObject( 'authcheckL10n', window.authcheckL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.8.0 + * @deprecated 5.5.0 + */ +window.tagsl10n = { + noPerm: '', + broken: '' +}; + +window.tagsl10n = window.tagsl10n || deprecateL10nObject( 'tagsl10n', window.tagsl10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.5.0 + * @deprecated 5.5.0 + */ +window.adminCommentsL10n = window.adminCommentsL10n || { + hotkeys_highlight_first: { + alternative: 'window.adminCommentsSettings.hotkeys_highlight_first', + func: function() { return window.adminCommentsSettings.hotkeys_highlight_first; } + }, + hotkeys_highlight_last: { + alternative: 'window.adminCommentsSettings.hotkeys_highlight_last', + func: function() { return window.adminCommentsSettings.hotkeys_highlight_last; } + }, + replyApprove: '', + reply: '', + warnQuickEdit: '', + warnCommentChanges: '', + docTitleComments: '', + docTitleCommentsCount: '' +}; + +window.adminCommentsL10n = deprecateL10nObject( 'adminCommentsL10n', window.adminCommentsL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.5.0 + * @deprecated 5.5.0 + */ +window.tagsSuggestL10n = window.tagsSuggestL10n || { + tagDelimiter: '', + removeTerm: '', + termSelected: '', + termAdded: '', + termRemoved: '' +}; + +window.tagsSuggestL10n = deprecateL10nObject( 'tagsSuggestL10n', window.tagsSuggestL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 3.5.0 + * @deprecated 5.5.0 + */ +window.wpColorPickerL10n = window.wpColorPickerL10n || { + clear: '', + clearAriaLabel: '', + defaultString: '', + defaultAriaLabel: '', + pick: '', + defaultLabel: '' +}; + +window.wpColorPickerL10n = deprecateL10nObject( 'wpColorPickerL10n', window.wpColorPickerL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.7.0 + * @deprecated 5.5.0 + */ +window.attachMediaBoxL10n = window.attachMediaBoxL10n || { + error: '' +}; + +window.attachMediaBoxL10n = deprecateL10nObject( 'attachMediaBoxL10n', window.attachMediaBoxL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.5.0 + * @deprecated 5.5.0 + */ +window.postL10n = window.postL10n || { + ok: '', + cancel: '', + publishOn: '', + publishOnFuture: '', + publishOnPast: '', + dateFormat: '', + showcomm: '', + endcomm: '', + publish: '', + schedule: '', + update: '', + savePending: '', + saveDraft: '', + 'private': '', + 'public': '', + publicSticky: '', + password: '', + privatelyPublished: '', + published: '', + saveAlert: '', + savingText: '', + permalinkSaved: '' +}; + +window.postL10n = deprecateL10nObject( 'postL10n', window.postL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.7.0 + * @deprecated 5.5.0 + */ +window.inlineEditL10n = window.inlineEditL10n || { + error: '', + ntdeltitle: '', + notitle: '', + comma: '', + saved: '' +}; + +window.inlineEditL10n = deprecateL10nObject( 'inlineEditL10n', window.inlineEditL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.7.0 + * @deprecated 5.5.0 + */ +window.plugininstallL10n = window.plugininstallL10n || { + plugin_information: '', + plugin_modal_label: '', + ays: '' +}; + +window.plugininstallL10n = deprecateL10nObject( 'plugininstallL10n', window.plugininstallL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 3.0.0 + * @deprecated 5.5.0 + */ +window.navMenuL10n = window.navMenuL10n || { + noResultsFound: '', + warnDeleteMenu: '', + saveAlert: '', + untitled: '' +}; + +window.navMenuL10n = deprecateL10nObject( 'navMenuL10n', window.navMenuL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.5.0 + * @deprecated 5.5.0 + */ +window.commentL10n = window.commentL10n || { + submittedOn: '', + dateFormat: '' +}; + +window.commentL10n = deprecateL10nObject( 'commentL10n', window.commentL10n, '5.5.0' ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 2.9.0 + * @deprecated 5.5.0 + */ +window.setPostThumbnailL10n = window.setPostThumbnailL10n || { + setThumbnail: '', + saving: '', + error: '', + done: '' +}; + +window.setPostThumbnailL10n = deprecateL10nObject( 'setPostThumbnailL10n', window.setPostThumbnailL10n, '5.5.0' ); + +/** + * Removed in 3.3.0, needed for back-compatibility. + * + * @since 2.7.0 + * @deprecated 3.3.0 + */ +window.adminMenu = { + init : function() {}, + fold : function() {}, + restoreMenuState : function() {}, + toggle : function() {}, + favorites : function() {} +}; + +// Show/hide/save table columns. +window.columns = { + + /** + * Initializes the column toggles in the screen options. + * + * Binds an onClick event to the checkboxes to show or hide the table columns + * based on their toggled state. And persists the toggled state. + * + * @since 2.7.0 + * + * @return {void} + */ + init : function() { + var that = this; + $('.hide-column-tog', '#adv-settings').on( 'click', function() { + var $t = $(this), column = $t.val(); + if ( $t.prop('checked') ) + that.checked(column); + else + that.unchecked(column); + + columns.saveManageColumnsState(); + }); + }, + + /** + * Saves the toggled state for the columns. + * + * Saves whether the columns should be shown or hidden on a page. + * + * @since 3.0.0 + * + * @return {void} + */ + saveManageColumnsState : function() { + var hidden = this.hidden(); + $.post(ajaxurl, { + action: 'hidden-columns', + hidden: hidden, + screenoptionnonce: $('#screenoptionnonce').val(), + page: pagenow + }); + }, + + /** + * Makes a column visible and adjusts the column span for the table. + * + * @since 3.0.0 + * @param {string} column The column name. + * + * @return {void} + */ + checked : function(column) { + $('.column-' + column).removeClass( 'hidden' ); + this.colSpanChange(+1); + }, + + /** + * Hides a column and adjusts the column span for the table. + * + * @since 3.0.0 + * @param {string} column The column name. + * + * @return {void} + */ + unchecked : function(column) { + $('.column-' + column).addClass( 'hidden' ); + this.colSpanChange(-1); + }, + + /** + * Gets all hidden columns. + * + * @since 3.0.0 + * + * @return {string} The hidden column names separated by a comma. + */ + hidden : function() { + return $( '.manage-column[id]' ).filter( '.hidden' ).map(function() { + return this.id; + }).get().join( ',' ); + }, + + /** + * Gets the checked column toggles from the screen options. + * + * @since 3.0.0 + * + * @return {string} String containing the checked column names. + */ + useCheckboxesForHidden : function() { + this.hidden = function(){ + return $('.hide-column-tog').not(':checked').map(function() { + var id = this.id; + return id.substring( id, id.length - 5 ); + }).get().join(','); + }; + }, + + /** + * Adjusts the column span for the table. + * + * @since 3.1.0 + * + * @param {number} diff The modifier for the column span. + */ + colSpanChange : function(diff) { + var $t = $('table').find('.colspanchange'), n; + if ( !$t.length ) + return; + n = parseInt( $t.attr('colspan'), 10 ) + diff; + $t.attr('colspan', n.toString()); + } +}; + +$( function() { columns.init(); } ); + +/** + * Validates that the required form fields are not empty. + * + * @since 2.9.0 + * + * @param {jQuery} form The form to validate. + * + * @return {boolean} Returns true if all required fields are not an empty string. + */ +window.validateForm = function( form ) { + return !$( form ) + .find( '.form-required' ) + .filter( function() { return $( ':input:visible', this ).val() === ''; } ) + .addClass( 'form-invalid' ) + .find( ':input:visible' ) + .on( 'change', function() { $( this ).closest( '.form-invalid' ).removeClass( 'form-invalid' ); } ) + .length; +}; + +// Stub for doing better warnings. +/** + * Shows message pop-up notice or confirmation message. + * + * @since 2.7.0 + * + * @type {{warn: showNotice.warn, note: showNotice.note}} + * + * @return {void} + */ +window.showNotice = { + + /** + * Shows a delete confirmation pop-up message. + * + * @since 2.7.0 + * + * @return {boolean} Returns true if the message is confirmed. + */ + warn : function() { + if ( confirm( __( 'You are about to permanently delete these items from your site.\nThis action cannot be undone.\n\'Cancel\' to stop, \'OK\' to delete.' ) ) ) { + return true; + } + + return false; + }, + + /** + * Shows an alert message. + * + * @since 2.7.0 + * + * @param text The text to display in the message. + */ + note : function(text) { + alert(text); + } +}; + +/** + * Represents the functions for the meta screen options panel. + * + * @since 3.2.0 + * + * @type {{element: null, toggles: null, page: null, init: screenMeta.init, + * toggleEvent: screenMeta.toggleEvent, open: screenMeta.open, + * close: screenMeta.close}} + * + * @return {void} + */ +window.screenMeta = { + element: null, // #screen-meta + toggles: null, // .screen-meta-toggle + page: null, // #wpcontent + + /** + * Initializes the screen meta options panel. + * + * @since 3.2.0 + * + * @return {void} + */ + init: function() { + this.element = $('#screen-meta'); + this.toggles = $( '#screen-meta-links' ).find( '.show-settings' ); + this.page = $('#wpcontent'); + + this.toggles.on( 'click', this.toggleEvent ); + }, + + /** + * Toggles the screen meta options panel. + * + * @since 3.2.0 + * + * @return {void} + */ + toggleEvent: function() { + var panel = $( '#' + $( this ).attr( 'aria-controls' ) ); + + if ( !panel.length ) + return; + + if ( panel.is(':visible') ) + screenMeta.close( panel, $(this) ); + else + screenMeta.open( panel, $(this) ); + }, + + /** + * Opens the screen meta options panel. + * + * @since 3.2.0 + * + * @param {jQuery} panel The screen meta options panel div. + * @param {jQuery} button The toggle button. + * + * @return {void} + */ + open: function( panel, button ) { + + $( '#screen-meta-links' ).find( '.screen-meta-toggle' ).not( button.parent() ).css( 'visibility', 'hidden' ); + + panel.parent().show(); + + /** + * Sets the focus to the meta options panel and adds the necessary CSS classes. + * + * @since 3.2.0 + * + * @return {void} + */ + panel.slideDown( 'fast', function() { + panel.removeClass( 'hidden' ).trigger( 'focus' ); + button.addClass( 'screen-meta-active' ).attr( 'aria-expanded', true ); + }); + + $document.trigger( 'screen:options:open' ); + }, + + /** + * Closes the screen meta options panel. + * + * @since 3.2.0 + * + * @param {jQuery} panel The screen meta options panel div. + * @param {jQuery} button The toggle button. + * + * @return {void} + */ + close: function( panel, button ) { + /** + * Hides the screen meta options panel. + * + * @since 3.2.0 + * + * @return {void} + */ + panel.slideUp( 'fast', function() { + button.removeClass( 'screen-meta-active' ).attr( 'aria-expanded', false ); + $('.screen-meta-toggle').css('visibility', ''); + panel.parent().hide(); + panel.addClass( 'hidden' ); + }); + + $document.trigger( 'screen:options:close' ); + } +}; + +/** + * Initializes the help tabs in the help panel. + * + * @param {Event} e The event object. + * + * @return {void} + */ +$('.contextual-help-tabs').on( 'click', 'a', function(e) { + var link = $(this), + panel; + + e.preventDefault(); + + // Don't do anything if the click is for the tab already showing. + if ( link.is('.active a') ) + return false; + + // Links. + $('.contextual-help-tabs .active').removeClass('active'); + link.parent('li').addClass('active'); + + panel = $( link.attr('href') ); + + // Panels. + $('.help-tab-content').not( panel ).removeClass('active').hide(); + panel.addClass('active').show(); +}); + +/** + * Update custom permalink structure via buttons. + */ +var permalinkStructureFocused = false, + $permalinkStructure = $( '#permalink_structure' ), + $permalinkStructureInputs = $( '.permalink-structure input:radio' ), + $permalinkCustomSelection = $( '#custom_selection' ), + $availableStructureTags = $( '.form-table.permalink-structure .available-structure-tags button' ); + +// Change permalink structure input when selecting one of the common structures. +$permalinkStructureInputs.on( 'change', function() { + if ( 'custom' === this.value ) { + return; + } + + $permalinkStructure.val( this.value ); + + // Update button states after selection. + $availableStructureTags.each( function() { + changeStructureTagButtonState( $( this ) ); + } ); +} ); + +$permalinkStructure.on( 'click input', function() { + $permalinkCustomSelection.prop( 'checked', true ); +} ); + +// Check if the permalink structure input field has had focus at least once. +$permalinkStructure.on( 'focus', function( event ) { + permalinkStructureFocused = true; + $( this ).off( event ); +} ); + +/** + * Enables or disables a structure tag button depending on its usage. + * + * If the structure is already used in the custom permalink structure, + * it will be disabled. + * + * @param {Object} button Button jQuery object. + */ +function changeStructureTagButtonState( button ) { + if ( -1 !== $permalinkStructure.val().indexOf( button.text().trim() ) ) { + button.attr( 'data-label', button.attr( 'aria-label' ) ); + button.attr( 'aria-label', button.attr( 'data-used' ) ); + button.attr( 'aria-pressed', true ); + button.addClass( 'active' ); + } else if ( button.attr( 'data-label' ) ) { + button.attr( 'aria-label', button.attr( 'data-label' ) ); + button.attr( 'aria-pressed', false ); + button.removeClass( 'active' ); + } +} + +// Check initial button state. +$availableStructureTags.each( function() { + changeStructureTagButtonState( $( this ) ); +} ); + +// Observe permalink structure field and disable buttons of tags that are already present. +$permalinkStructure.on( 'change', function() { + $availableStructureTags.each( function() { + changeStructureTagButtonState( $( this ) ); + } ); +} ); + +$availableStructureTags.on( 'click', function() { + var permalinkStructureValue = $permalinkStructure.val(), + selectionStart = $permalinkStructure[ 0 ].selectionStart, + selectionEnd = $permalinkStructure[ 0 ].selectionEnd, + textToAppend = $( this ).text().trim(), + textToAnnounce, + newSelectionStart; + + if ( $( this ).hasClass( 'active' ) ) { + textToAnnounce = $( this ).attr( 'data-removed' ); + } else { + textToAnnounce = $( this ).attr( 'data-added' ); + } + + // Remove structure tag if already part of the structure. + if ( -1 !== permalinkStructureValue.indexOf( textToAppend ) ) { + permalinkStructureValue = permalinkStructureValue.replace( textToAppend + '/', '' ); + + $permalinkStructure.val( '/' === permalinkStructureValue ? '' : permalinkStructureValue ); + + // Announce change to screen readers. + $( '#custom_selection_updated' ).text( textToAnnounce ); + + // Disable button. + changeStructureTagButtonState( $( this ) ); + + return; + } + + // Input field never had focus, move selection to end of input. + if ( ! permalinkStructureFocused && 0 === selectionStart && 0 === selectionEnd ) { + selectionStart = selectionEnd = permalinkStructureValue.length; + } + + $permalinkCustomSelection.prop( 'checked', true ); + + // Prepend and append slashes if necessary. + if ( '/' !== permalinkStructureValue.substr( 0, selectionStart ).substr( -1 ) ) { + textToAppend = '/' + textToAppend; + } + + if ( '/' !== permalinkStructureValue.substr( selectionEnd, 1 ) ) { + textToAppend = textToAppend + '/'; + } + + // Insert structure tag at the specified position. + $permalinkStructure.val( permalinkStructureValue.substr( 0, selectionStart ) + textToAppend + permalinkStructureValue.substr( selectionEnd ) ); + + // Announce change to screen readers. + $( '#custom_selection_updated' ).text( textToAnnounce ); + + // Disable button. + changeStructureTagButtonState( $( this ) ); + + // If input had focus give it back with cursor right after appended text. + if ( permalinkStructureFocused && $permalinkStructure[0].setSelectionRange ) { + newSelectionStart = ( permalinkStructureValue.substr( 0, selectionStart ) + textToAppend ).length; + $permalinkStructure[0].setSelectionRange( newSelectionStart, newSelectionStart ); + $permalinkStructure.trigger( 'focus' ); + } +} ); + +$( function() { + var checks, first, last, checked, sliced, mobileEvent, transitionTimeout, focusedRowActions, + lastClicked = false, + pageInput = $('input.current-page'), + currentPage = pageInput.val(), + isIOS = /iPhone|iPad|iPod/.test( navigator.userAgent ), + isAndroid = navigator.userAgent.indexOf( 'Android' ) !== -1, + $adminMenuWrap = $( '#adminmenuwrap' ), + $wpwrap = $( '#wpwrap' ), + $adminmenu = $( '#adminmenu' ), + $overlay = $( '#wp-responsive-overlay' ), + $toolbar = $( '#wp-toolbar' ), + $toolbarPopups = $toolbar.find( 'a[aria-haspopup="true"]' ), + $sortables = $('.meta-box-sortables'), + wpResponsiveActive = false, + $adminbar = $( '#wpadminbar' ), + lastScrollPosition = 0, + pinnedMenuTop = false, + pinnedMenuBottom = false, + menuTop = 0, + menuState, + menuIsPinned = false, + height = { + window: $window.height(), + wpwrap: $wpwrap.height(), + adminbar: $adminbar.height(), + menu: $adminMenuWrap.height() + }, + $headerEnd = $( '.wp-header-end' ); + + /** + * Makes the fly-out submenu header clickable, when the menu is folded. + * + * @param {Event} e The event object. + * + * @return {void} + */ + $adminmenu.on('click.wp-submenu-head', '.wp-submenu-head', function(e){ + $(e.target).parent().siblings('a').get(0).click(); + }); + + /** + * Collapses the admin menu. + * + * @return {void} + */ + $( '#collapse-button' ).on( 'click.collapse-menu', function() { + var viewportWidth = getViewportWidth() || 961; + + // Reset any compensation for submenus near the bottom of the screen. + $('#adminmenu div.wp-submenu').css('margin-top', ''); + + if ( viewportWidth <= 960 ) { + if ( $body.hasClass('auto-fold') ) { + $body.removeClass('auto-fold').removeClass('folded'); + setUserSetting('unfold', 1); + setUserSetting('mfold', 'o'); + menuState = 'open'; + } else { + $body.addClass('auto-fold'); + setUserSetting('unfold', 0); + menuState = 'folded'; + } + } else { + if ( $body.hasClass('folded') ) { + $body.removeClass('folded'); + setUserSetting('mfold', 'o'); + menuState = 'open'; + } else { + $body.addClass('folded'); + setUserSetting('mfold', 'f'); + menuState = 'folded'; + } + } + + $document.trigger( 'wp-collapse-menu', { state: menuState } ); + }); + + /** + * Handles the `aria-haspopup` attribute on the current menu item when it has a submenu. + * + * @since 4.4.0 + * + * @return {void} + */ + function currentMenuItemHasPopup() { + var $current = $( 'a.wp-has-current-submenu' ); + + if ( 'folded' === menuState ) { + // When folded or auto-folded and not responsive view, the current menu item does have a fly-out sub-menu. + $current.attr( 'aria-haspopup', 'true' ); + } else { + // When expanded or in responsive view, reset aria-haspopup. + $current.attr( 'aria-haspopup', 'false' ); + } + } + + $document.on( 'wp-menu-state-set wp-collapse-menu wp-responsive-activate wp-responsive-deactivate', currentMenuItemHasPopup ); + + /** + * Ensures an admin submenu is within the visual viewport. + * + * @since 4.1.0 + * + * @param {jQuery} $menuItem The parent menu item containing the submenu. + * + * @return {void} + */ + function adjustSubmenu( $menuItem ) { + var bottomOffset, pageHeight, adjustment, theFold, menutop, wintop, maxtop, + $submenu = $menuItem.find( '.wp-submenu' ); + + menutop = $menuItem.offset().top; + wintop = $window.scrollTop(); + maxtop = menutop - wintop - 30; // max = make the top of the sub almost touch admin bar. + + bottomOffset = menutop + $submenu.height() + 1; // Bottom offset of the menu. + pageHeight = $wpwrap.height(); // Height of the entire page. + adjustment = 60 + bottomOffset - pageHeight; + theFold = $window.height() + wintop - 50; // The fold. + + if ( theFold < ( bottomOffset - adjustment ) ) { + adjustment = bottomOffset - theFold; + } + + if ( adjustment > maxtop ) { + adjustment = maxtop; + } + + if ( adjustment > 1 && $('#wp-admin-bar-menu-toggle').is(':hidden') ) { + $submenu.css( 'margin-top', '-' + adjustment + 'px' ); + } else { + $submenu.css( 'margin-top', '' ); + } + } + + if ( 'ontouchstart' in window || /IEMobile\/[1-9]/.test(navigator.userAgent) ) { // Touch screen device. + // iOS Safari works with touchstart, the rest work with click. + mobileEvent = isIOS ? 'touchstart' : 'click'; + + /** + * Closes any open submenus when touch/click is not on the menu. + * + * @param {Event} e The event object. + * + * @return {void} + */ + $body.on( mobileEvent+'.wp-mobile-hover', function(e) { + if ( $adminmenu.data('wp-responsive') ) { + return; + } + + if ( ! $( e.target ).closest( '#adminmenu' ).length ) { + $adminmenu.find( 'li.opensub' ).removeClass( 'opensub' ); + } + }); + + /** + * Handles the opening or closing the submenu based on the mobile click|touch event. + * + * @param {Event} event The event object. + * + * @return {void} + */ + $adminmenu.find( 'a.wp-has-submenu' ).on( mobileEvent + '.wp-mobile-hover', function( event ) { + var $menuItem = $(this).parent(); + + if ( $adminmenu.data( 'wp-responsive' ) ) { + return; + } + + /* + * Show the sub instead of following the link if: + * - the submenu is not open. + * - the submenu is not shown inline or the menu is not folded. + */ + if ( ! $menuItem.hasClass( 'opensub' ) && ( ! $menuItem.hasClass( 'wp-menu-open' ) || $menuItem.width() < 40 ) ) { + event.preventDefault(); + adjustSubmenu( $menuItem ); + $adminmenu.find( 'li.opensub' ).removeClass( 'opensub' ); + $menuItem.addClass('opensub'); + } + }); + } + + if ( ! isIOS && ! isAndroid ) { + $adminmenu.find( 'li.wp-has-submenu' ).hoverIntent({ + + /** + * Opens the submenu when hovered over the menu item for desktops. + * + * @return {void} + */ + over: function() { + var $menuItem = $( this ), + $submenu = $menuItem.find( '.wp-submenu' ), + top = parseInt( $submenu.css( 'top' ), 10 ); + + if ( isNaN( top ) || top > -5 ) { // The submenu is visible. + return; + } + + if ( $adminmenu.data( 'wp-responsive' ) ) { + // The menu is in responsive mode, bail. + return; + } + + adjustSubmenu( $menuItem ); + $adminmenu.find( 'li.opensub' ).removeClass( 'opensub' ); + $menuItem.addClass( 'opensub' ); + }, + + /** + * Closes the submenu when no longer hovering the menu item. + * + * @return {void} + */ + out: function(){ + if ( $adminmenu.data( 'wp-responsive' ) ) { + // The menu is in responsive mode, bail. + return; + } + + $( this ).removeClass( 'opensub' ).find( '.wp-submenu' ).css( 'margin-top', '' ); + }, + timeout: 200, + sensitivity: 7, + interval: 90 + }); + + /** + * Opens the submenu on when focused on the menu item. + * + * @param {Event} event The event object. + * + * @return {void} + */ + $adminmenu.on( 'focus.adminmenu', '.wp-submenu a', function( event ) { + if ( $adminmenu.data( 'wp-responsive' ) ) { + // The menu is in responsive mode, bail. + return; + } + + $( event.target ).closest( 'li.menu-top' ).addClass( 'opensub' ); + + /** + * Closes the submenu on blur from the menu item. + * + * @param {Event} event The event object. + * + * @return {void} + */ + }).on( 'blur.adminmenu', '.wp-submenu a', function( event ) { + if ( $adminmenu.data( 'wp-responsive' ) ) { + return; + } + + $( event.target ).closest( 'li.menu-top' ).removeClass( 'opensub' ); + + /** + * Adjusts the size for the submenu. + * + * @return {void} + */ + }).find( 'li.wp-has-submenu.wp-not-current-submenu' ).on( 'focusin.adminmenu', function() { + adjustSubmenu( $( this ) ); + }); + } + + /* + * The `.below-h2` class is here just for backward compatibility with plugins + * that are (incorrectly) using it. Do not use. Use `.inline` instead. See #34570. + * If '.wp-header-end' is found, append the notices after it otherwise + * after the first h1 or h2 heading found within the main content. + */ + if ( ! $headerEnd.length ) { + $headerEnd = $( '.wrap h1, .wrap h2' ).first(); + } + $( 'div.updated, div.error, div.notice' ).not( '.inline, .below-h2' ).insertAfter( $headerEnd ); + + /** + * Makes notices dismissible. + * + * @since 4.4.0 + * + * @return {void} + */ + function makeNoticesDismissible() { + $( '.notice.is-dismissible' ).each( function() { + var $el = $( this ), + $button = $( '<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>' ); + + if ( $el.find( '.notice-dismiss' ).length ) { + return; + } + + // Ensure plain text. + $button.find( '.screen-reader-text' ).text( __( 'Dismiss this notice.' ) ); + $button.on( 'click.wp-dismiss-notice', function( event ) { + event.preventDefault(); + $el.fadeTo( 100, 0, function() { + $el.slideUp( 100, function() { + $el.remove(); + }); + }); + }); + + $el.append( $button ); + }); + } + + $document.on( 'wp-updates-notice-added wp-plugin-install-error wp-plugin-update-error wp-plugin-delete-error wp-theme-install-error wp-theme-delete-error', makeNoticesDismissible ); + + // Init screen meta. + screenMeta.init(); + + /** + * Checks a checkbox. + * + * This event needs to be delegated. Ticket #37973. + * + * @return {boolean} Returns whether a checkbox is checked or not. + */ + $body.on( 'click', 'tbody > tr > .check-column :checkbox', function( event ) { + // Shift click to select a range of checkboxes. + if ( 'undefined' == event.shiftKey ) { return true; } + if ( event.shiftKey ) { + if ( !lastClicked ) { return true; } + checks = $( lastClicked ).closest( 'form' ).find( ':checkbox' ).filter( ':visible:enabled' ); + first = checks.index( lastClicked ); + last = checks.index( this ); + checked = $(this).prop('checked'); + if ( 0 < first && 0 < last && first != last ) { + sliced = ( last > first ) ? checks.slice( first, last ) : checks.slice( last, first ); + sliced.prop( 'checked', function() { + if ( $(this).closest('tr').is(':visible') ) + return checked; + + return false; + }); + } + } + lastClicked = this; + + // Toggle the "Select all" checkboxes depending if the other ones are all checked or not. + var unchecked = $(this).closest('tbody').find(':checkbox').filter(':visible:enabled').not(':checked'); + + /** + * Determines if all checkboxes are checked. + * + * @return {boolean} Returns true if there are no unchecked checkboxes. + */ + $(this).closest('table').children('thead, tfoot').find(':checkbox').prop('checked', function() { + return ( 0 === unchecked.length ); + }); + + return true; + }); + + /** + * Controls all the toggles on bulk toggle change. + * + * When the bulk checkbox is changed, all the checkboxes in the tables are changed accordingly. + * When the shift-button is pressed while changing the bulk checkbox the checkboxes in the table are inverted. + * + * This event needs to be delegated. Ticket #37973. + * + * @param {Event} event The event object. + * + * @return {boolean} + */ + $body.on( 'click.wp-toggle-checkboxes', 'thead .check-column :checkbox, tfoot .check-column :checkbox', function( event ) { + var $this = $(this), + $table = $this.closest( 'table' ), + controlChecked = $this.prop('checked'), + toggle = event.shiftKey || $this.data('wp-toggle'); + + $table.children( 'tbody' ).filter(':visible') + .children().children('.check-column').find(':checkbox') + /** + * Updates the checked state on the checkbox in the table. + * + * @return {boolean} True checks the checkbox, False unchecks the checkbox. + */ + .prop('checked', function() { + if ( $(this).is(':hidden,:disabled') ) { + return false; + } + + if ( toggle ) { + return ! $(this).prop( 'checked' ); + } else if ( controlChecked ) { + return true; + } + + return false; + }); + + $table.children('thead, tfoot').filter(':visible') + .children().children('.check-column').find(':checkbox') + + /** + * Syncs the bulk checkboxes on the top and bottom of the table. + * + * @return {boolean} True checks the checkbox, False unchecks the checkbox. + */ + .prop('checked', function() { + if ( toggle ) { + return false; + } else if ( controlChecked ) { + return true; + } + + return false; + }); + }); + + /** + * Marries a secondary control to its primary control. + * + * @param {jQuery} topSelector The top selector element. + * @param {jQuery} topSubmit The top submit element. + * @param {jQuery} bottomSelector The bottom selector element. + * @param {jQuery} bottomSubmit The bottom submit element. + * @return {void} + */ + function marryControls( topSelector, topSubmit, bottomSelector, bottomSubmit ) { + /** + * Updates the primary selector when the secondary selector is changed. + * + * @since 5.7.0 + * + * @return {void} + */ + function updateTopSelector() { + topSelector.val($(this).val()); + } + bottomSelector.on('change', updateTopSelector); + + /** + * Updates the secondary selector when the primary selector is changed. + * + * @since 5.7.0 + * + * @return {void} + */ + function updateBottomSelector() { + bottomSelector.val($(this).val()); + } + topSelector.on('change', updateBottomSelector); + + /** + * Triggers the primary submit when then secondary submit is clicked. + * + * @since 5.7.0 + * + * @return {void} + */ + function triggerSubmitClick(e) { + e.preventDefault(); + e.stopPropagation(); + + topSubmit.trigger('click'); + } + bottomSubmit.on('click', triggerSubmitClick); + } + + // Marry the secondary "Bulk actions" controls to the primary controls: + marryControls( $('#bulk-action-selector-top'), $('#doaction'), $('#bulk-action-selector-bottom'), $('#doaction2') ); + + // Marry the secondary "Change role to" controls to the primary controls: + marryControls( $('#new_role'), $('#changeit'), $('#new_role2'), $('#changeit2') ); + + /** + * Shows row actions on focus of its parent container element or any other elements contained within. + * + * @return {void} + */ + $( '#wpbody-content' ).on({ + focusin: function() { + clearTimeout( transitionTimeout ); + focusedRowActions = $( this ).find( '.row-actions' ); + // transitionTimeout is necessary for Firefox, but Chrome won't remove the CSS class without a little help. + $( '.row-actions' ).not( this ).removeClass( 'visible' ); + focusedRowActions.addClass( 'visible' ); + }, + focusout: function() { + // Tabbing between post title and .row-actions links needs a brief pause, otherwise + // the .row-actions div gets hidden in transit in some browsers (ahem, Firefox). + transitionTimeout = setTimeout( function() { + focusedRowActions.removeClass( 'visible' ); + }, 30 ); + } + }, '.table-view-list .has-row-actions' ); + + // Toggle list table rows on small screens. + $( 'tbody' ).on( 'click', '.toggle-row', function() { + $( this ).closest( 'tr' ).toggleClass( 'is-expanded' ); + }); + + $('#default-password-nag-no').on( 'click', function() { + setUserSetting('default_password_nag', 'hide'); + $('div.default-password-nag').hide(); + return false; + }); + + /** + * Handles tab keypresses in theme and plugin file editor textareas. + * + * @param {Event} e The event object. + * + * @return {void} + */ + $('#newcontent').on('keydown.wpevent_InsertTab', function(e) { + var el = e.target, selStart, selEnd, val, scroll, sel; + + // After pressing escape key (keyCode: 27), the tab key should tab out of the textarea. + if ( e.keyCode == 27 ) { + // When pressing Escape: Opera 12 and 27 blur form fields, IE 8 clears them. + e.preventDefault(); + $(el).data('tab-out', true); + return; + } + + // Only listen for plain tab key (keyCode: 9) without any modifiers. + if ( e.keyCode != 9 || e.ctrlKey || e.altKey || e.shiftKey ) + return; + + // After tabbing out, reset it so next time the tab key can be used again. + if ( $(el).data('tab-out') ) { + $(el).data('tab-out', false); + return; + } + + selStart = el.selectionStart; + selEnd = el.selectionEnd; + val = el.value; + + // If any text is selected, replace the selection with a tab character. + if ( document.selection ) { + el.focus(); + sel = document.selection.createRange(); + sel.text = '\t'; + } else if ( selStart >= 0 ) { + scroll = this.scrollTop; + el.value = val.substring(0, selStart).concat('\t', val.substring(selEnd) ); + el.selectionStart = el.selectionEnd = selStart + 1; + this.scrollTop = scroll; + } + + // Cancel the regular tab functionality, to prevent losing focus of the textarea. + if ( e.stopPropagation ) + e.stopPropagation(); + if ( e.preventDefault ) + e.preventDefault(); + }); + + // Reset page number variable for new filters/searches but not for bulk actions. See #17685. + if ( pageInput.length ) { + + /** + * Handles pagination variable when filtering the list table. + * + * Set the pagination argument to the first page when the post-filter form is submitted. + * This happens when pressing the 'filter' button on the list table page. + * + * The pagination argument should not be touched when the bulk action dropdowns are set to do anything. + * + * The form closest to the pageInput is the post-filter form. + * + * @return {void} + */ + pageInput.closest('form').on( 'submit', function() { + /* + * action = bulk action dropdown at the top of the table + */ + if ( $('select[name="action"]').val() == -1 && pageInput.val() == currentPage ) + pageInput.val('1'); + }); + } + + /** + * Resets the bulk actions when the search button is clicked. + * + * @return {void} + */ + $('.search-box input[type="search"], .search-box input[type="submit"]').on( 'mousedown', function () { + $('select[name^="action"]').val('-1'); + }); + + /** + * Scrolls into view when focus.scroll-into-view is triggered. + * + * @param {Event} e The event object. + * + * @return {void} + */ + $('#contextual-help-link, #show-settings-link').on( 'focus.scroll-into-view', function(e){ + if ( e.target.scrollIntoViewIfNeeded ) + e.target.scrollIntoViewIfNeeded(false); + }); + + /** + * Disables the submit upload buttons when no data is entered. + * + * @return {void} + */ + (function(){ + var button, input, form = $('form.wp-upload-form'); + + // Exit when no upload form is found. + if ( ! form.length ) + return; + + button = form.find('input[type="submit"]'); + input = form.find('input[type="file"]'); + + /** + * Determines if any data is entered in any file upload input. + * + * @since 3.5.0 + * + * @return {void} + */ + function toggleUploadButton() { + // When no inputs have a value, disable the upload buttons. + button.prop('disabled', '' === input.map( function() { + return $(this).val(); + }).get().join('')); + } + + // Update the status initially. + toggleUploadButton(); + // Update the status when any file input changes. + input.on('change', toggleUploadButton); + })(); + + /** + * Pins the menu while distraction-free writing is enabled. + * + * @param {Event} event Event data. + * + * @since 4.1.0 + * + * @return {void} + */ + function pinMenu( event ) { + var windowPos = $window.scrollTop(), + resizing = ! event || event.type !== 'scroll'; + + if ( isIOS || $adminmenu.data( 'wp-responsive' ) ) { + return; + } + + /* + * When the menu is higher than the window and smaller than the entire page. + * It should be adjusted to be able to see the entire menu. + * + * Otherwise it can be accessed normally. + */ + if ( height.menu + height.adminbar < height.window || + height.menu + height.adminbar + 20 > height.wpwrap ) { + unpinMenu(); + return; + } + + menuIsPinned = true; + + // If the menu is higher than the window, compensate on scroll. + if ( height.menu + height.adminbar > height.window ) { + // Check for overscrolling, this happens when swiping up at the top of the document in modern browsers. + if ( windowPos < 0 ) { + // Stick the menu to the top. + if ( ! pinnedMenuTop ) { + pinnedMenuTop = true; + pinnedMenuBottom = false; + + $adminMenuWrap.css({ + position: 'fixed', + top: '', + bottom: '' + }); + } + + return; + } else if ( windowPos + height.window > $document.height() - 1 ) { + // When overscrolling at the bottom, stick the menu to the bottom. + if ( ! pinnedMenuBottom ) { + pinnedMenuBottom = true; + pinnedMenuTop = false; + + $adminMenuWrap.css({ + position: 'fixed', + top: '', + bottom: 0 + }); + } + + return; + } + + if ( windowPos > lastScrollPosition ) { + // When a down scroll has been detected. + + // If it was pinned to the top, unpin and calculate relative scroll. + if ( pinnedMenuTop ) { + pinnedMenuTop = false; + // Calculate new offset position. + menuTop = $adminMenuWrap.offset().top - height.adminbar - ( windowPos - lastScrollPosition ); + + if ( menuTop + height.menu + height.adminbar < windowPos + height.window ) { + menuTop = windowPos + height.window - height.menu - height.adminbar; + } + + $adminMenuWrap.css({ + position: 'absolute', + top: menuTop, + bottom: '' + }); + } else if ( ! pinnedMenuBottom && $adminMenuWrap.offset().top + height.menu < windowPos + height.window ) { + // Pin it to the bottom. + pinnedMenuBottom = true; + + $adminMenuWrap.css({ + position: 'fixed', + top: '', + bottom: 0 + }); + } + } else if ( windowPos < lastScrollPosition ) { + // When a scroll up is detected. + + // If it was pinned to the bottom, unpin and calculate relative scroll. + if ( pinnedMenuBottom ) { + pinnedMenuBottom = false; + + // Calculate new offset position. + menuTop = $adminMenuWrap.offset().top - height.adminbar + ( lastScrollPosition - windowPos ); + + if ( menuTop + height.menu > windowPos + height.window ) { + menuTop = windowPos; + } + + $adminMenuWrap.css({ + position: 'absolute', + top: menuTop, + bottom: '' + }); + } else if ( ! pinnedMenuTop && $adminMenuWrap.offset().top >= windowPos + height.adminbar ) { + + // Pin it to the top. + pinnedMenuTop = true; + + $adminMenuWrap.css({ + position: 'fixed', + top: '', + bottom: '' + }); + } + } else if ( resizing ) { + // Window is being resized. + + pinnedMenuTop = pinnedMenuBottom = false; + + // Calculate the new offset. + menuTop = windowPos + height.window - height.menu - height.adminbar - 1; + + if ( menuTop > 0 ) { + $adminMenuWrap.css({ + position: 'absolute', + top: menuTop, + bottom: '' + }); + } else { + unpinMenu(); + } + } + } + + lastScrollPosition = windowPos; + } + + /** + * Determines the height of certain elements. + * + * @since 4.1.0 + * + * @return {void} + */ + function resetHeights() { + height = { + window: $window.height(), + wpwrap: $wpwrap.height(), + adminbar: $adminbar.height(), + menu: $adminMenuWrap.height() + }; + } + + /** + * Unpins the menu. + * + * @since 4.1.0 + * + * @return {void} + */ + function unpinMenu() { + if ( isIOS || ! menuIsPinned ) { + return; + } + + pinnedMenuTop = pinnedMenuBottom = menuIsPinned = false; + $adminMenuWrap.css({ + position: '', + top: '', + bottom: '' + }); + } + + /** + * Pins and unpins the menu when applicable. + * + * @since 4.1.0 + * + * @return {void} + */ + function setPinMenu() { + resetHeights(); + + if ( $adminmenu.data('wp-responsive') ) { + $body.removeClass( 'sticky-menu' ); + unpinMenu(); + } else if ( height.menu + height.adminbar > height.window ) { + pinMenu(); + $body.removeClass( 'sticky-menu' ); + } else { + $body.addClass( 'sticky-menu' ); + unpinMenu(); + } + } + + if ( ! isIOS ) { + $window.on( 'scroll.pin-menu', pinMenu ); + $document.on( 'tinymce-editor-init.pin-menu', function( event, editor ) { + editor.on( 'wp-autoresize', resetHeights ); + }); + } + + /** + * Changes the sortables and responsiveness of metaboxes. + * + * @since 3.8.0 + * + * @return {void} + */ + window.wpResponsive = { + + /** + * Initializes the wpResponsive object. + * + * @since 3.8.0 + * + * @return {void} + */ + init: function() { + var self = this; + + this.maybeDisableSortables = this.maybeDisableSortables.bind( this ); + + // Modify functionality based on custom activate/deactivate event. + $document.on( 'wp-responsive-activate.wp-responsive', function() { + self.activate(); + }).on( 'wp-responsive-deactivate.wp-responsive', function() { + self.deactivate(); + }); + + $( '#wp-admin-bar-menu-toggle a' ).attr( 'aria-expanded', 'false' ); + + // Toggle sidebar when toggle is clicked. + $( '#wp-admin-bar-menu-toggle' ).on( 'click.wp-responsive', function( event ) { + event.preventDefault(); + + // Close any open toolbar submenus. + $adminbar.find( '.hover' ).removeClass( 'hover' ); + + $wpwrap.toggleClass( 'wp-responsive-open' ); + if ( $wpwrap.hasClass( 'wp-responsive-open' ) ) { + $(this).find('a').attr( 'aria-expanded', 'true' ); + $( '#adminmenu a:first' ).trigger( 'focus' ); + } else { + $(this).find('a').attr( 'aria-expanded', 'false' ); + } + } ); + + // Close sidebar when target moves outside of toggle and sidebar. + $( document ).on( 'click', function( event ) { + if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) || ! document.hasFocus() ) { + return; + } + + var focusIsInToggle = $.contains( $( '#wp-admin-bar-menu-toggle' )[0], event.target ); + var focusIsInSidebar = $.contains( $( '#adminmenuwrap' )[0], event.target ); + + if ( ! focusIsInToggle && ! focusIsInSidebar ) { + $( '#wp-admin-bar-menu-toggle' ).trigger( 'click.wp-responsive' ); + } + } ); + + // Close sidebar when a keypress completes outside of toggle and sidebar. + $( document ).on( 'keyup', function( event ) { + var toggleButton = $( '#wp-admin-bar-menu-toggle' )[0]; + if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) ) { + return; + } + if ( 27 === event.keyCode ) { + $( toggleButton ).trigger( 'click.wp-responsive' ); + $( toggleButton ).find( 'a' ).trigger( 'focus' ); + } else { + if ( 9 === event.keyCode ) { + var sidebar = $( '#adminmenuwrap' )[0]; + var focusedElement = event.relatedTarget || document.activeElement; + // A brief delay is required to allow focus to switch to another element. + setTimeout( function() { + var focusIsInToggle = $.contains( toggleButton, focusedElement ); + var focusIsInSidebar = $.contains( sidebar, focusedElement ); + + if ( ! focusIsInToggle && ! focusIsInSidebar ) { + $( toggleButton ).trigger( 'click.wp-responsive' ); + } + }, 10 ); + } + } + }); + + // Add menu events. + $adminmenu.on( 'click.wp-responsive', 'li.wp-has-submenu > a', function( event ) { + if ( ! $adminmenu.data('wp-responsive') ) { + return; + } + + $( this ).parent( 'li' ).toggleClass( 'selected' ); + $( this ).trigger( 'focus' ); + event.preventDefault(); + }); + + self.trigger(); + $document.on( 'wp-window-resized.wp-responsive', this.trigger.bind( this ) ); + + // This needs to run later as UI Sortable may be initialized when the document is ready. + $window.on( 'load.wp-responsive', this.maybeDisableSortables ); + $document.on( 'postbox-toggled', this.maybeDisableSortables ); + + // When the screen columns are changed, potentially disable sortables. + $( '#screen-options-wrap input' ).on( 'click', this.maybeDisableSortables ); + }, + + /** + * Disable sortables if there is only one metabox, or the screen is in one column mode. Otherwise, enable sortables. + * + * @since 5.3.0 + * + * @return {void} + */ + maybeDisableSortables: function() { + var width = navigator.userAgent.indexOf('AppleWebKit/') > -1 ? $window.width() : window.innerWidth; + + if ( + ( width <= 782 ) || + ( 1 >= $sortables.find( '.ui-sortable-handle:visible' ).length && jQuery( '.columns-prefs-1 input' ).prop( 'checked' ) ) + ) { + this.disableSortables(); + } else { + this.enableSortables(); + } + }, + + /** + * Changes properties of body and admin menu. + * + * Pins and unpins the menu and adds the auto-fold class to the body. + * Makes the admin menu responsive and disables the metabox sortables. + * + * @since 3.8.0 + * + * @return {void} + */ + activate: function() { + setPinMenu(); + + if ( ! $body.hasClass( 'auto-fold' ) ) { + $body.addClass( 'auto-fold' ); + } + + $adminmenu.data( 'wp-responsive', 1 ); + this.disableSortables(); + }, + + /** + * Changes properties of admin menu and enables metabox sortables. + * + * Pin and unpin the menu. + * Removes the responsiveness of the admin menu and enables the metabox sortables. + * + * @since 3.8.0 + * + * @return {void} + */ + deactivate: function() { + setPinMenu(); + $adminmenu.removeData('wp-responsive'); + + this.maybeDisableSortables(); + }, + + /** + * Sets the responsiveness and enables the overlay based on the viewport width. + * + * @since 3.8.0 + * + * @return {void} + */ + trigger: function() { + var viewportWidth = getViewportWidth(); + + // Exclude IE < 9, it doesn't support @media CSS rules. + if ( ! viewportWidth ) { + return; + } + + if ( viewportWidth <= 782 ) { + if ( ! wpResponsiveActive ) { + $document.trigger( 'wp-responsive-activate' ); + wpResponsiveActive = true; + } + } else { + if ( wpResponsiveActive ) { + $document.trigger( 'wp-responsive-deactivate' ); + wpResponsiveActive = false; + } + } + + if ( viewportWidth <= 480 ) { + this.enableOverlay(); + } else { + this.disableOverlay(); + } + + this.maybeDisableSortables(); + }, + + /** + * Inserts a responsive overlay and toggles the window. + * + * @since 3.8.0 + * + * @return {void} + */ + enableOverlay: function() { + if ( $overlay.length === 0 ) { + $overlay = $( '<div id="wp-responsive-overlay"></div>' ) + .insertAfter( '#wpcontent' ) + .hide() + .on( 'click.wp-responsive', function() { + $toolbar.find( '.menupop.hover' ).removeClass( 'hover' ); + $( this ).hide(); + }); + } + + $toolbarPopups.on( 'click.wp-responsive', function() { + $overlay.show(); + }); + }, + + /** + * Disables the responsive overlay and removes the overlay. + * + * @since 3.8.0 + * + * @return {void} + */ + disableOverlay: function() { + $toolbarPopups.off( 'click.wp-responsive' ); + $overlay.hide(); + }, + + /** + * Disables sortables. + * + * @since 3.8.0 + * + * @return {void} + */ + disableSortables: function() { + if ( $sortables.length ) { + try { + $sortables.sortable( 'disable' ); + $sortables.find( '.ui-sortable-handle' ).addClass( 'is-non-sortable' ); + } catch ( e ) {} + } + }, + + /** + * Enables sortables. + * + * @since 3.8.0 + * + * @return {void} + */ + enableSortables: function() { + if ( $sortables.length ) { + try { + $sortables.sortable( 'enable' ); + $sortables.find( '.ui-sortable-handle' ).removeClass( 'is-non-sortable' ); + } catch ( e ) {} + } + } + }; + + /** + * Add an ARIA role `button` to elements that behave like UI controls when JavaScript is on. + * + * @since 4.5.0 + * + * @return {void} + */ + function aria_button_if_js() { + $( '.aria-button-if-js' ).attr( 'role', 'button' ); + } + + $( document ).on( 'ajaxComplete', function() { + aria_button_if_js(); + }); + + /** + * Get the viewport width. + * + * @since 4.7.0 + * + * @return {number|boolean} The current viewport width or false if the + * browser doesn't support innerWidth (IE < 9). + */ + function getViewportWidth() { + var viewportWidth = false; + + if ( window.innerWidth ) { + // On phones, window.innerWidth is affected by zooming. + viewportWidth = Math.max( window.innerWidth, document.documentElement.clientWidth ); + } + + return viewportWidth; + } + + /** + * Sets the admin menu collapsed/expanded state. + * + * Sets the global variable `menuState` and triggers a custom event passing + * the current menu state. + * + * @since 4.7.0 + * + * @return {void} + */ + function setMenuState() { + var viewportWidth = getViewportWidth() || 961; + + if ( viewportWidth <= 782 ) { + menuState = 'responsive'; + } else if ( $body.hasClass( 'folded' ) || ( $body.hasClass( 'auto-fold' ) && viewportWidth <= 960 && viewportWidth > 782 ) ) { + menuState = 'folded'; + } else { + menuState = 'open'; + } + + $document.trigger( 'wp-menu-state-set', { state: menuState } ); + } + + // Set the menu state when the window gets resized. + $document.on( 'wp-window-resized.set-menu-state', setMenuState ); + + /** + * Sets ARIA attributes on the collapse/expand menu button. + * + * When the admin menu is open or folded, updates the `aria-expanded` and + * `aria-label` attributes of the button to give feedback to assistive + * technologies. In the responsive view, the button is always hidden. + * + * @since 4.7.0 + * + * @return {void} + */ + $document.on( 'wp-menu-state-set wp-collapse-menu', function( event, eventData ) { + var $collapseButton = $( '#collapse-button' ), + ariaExpanded, ariaLabelText; + + if ( 'folded' === eventData.state ) { + ariaExpanded = 'false'; + ariaLabelText = __( 'Expand Main menu' ); + } else { + ariaExpanded = 'true'; + ariaLabelText = __( 'Collapse Main menu' ); + } + + $collapseButton.attr({ + 'aria-expanded': ariaExpanded, + 'aria-label': ariaLabelText + }); + }); + + window.wpResponsive.init(); + setPinMenu(); + setMenuState(); + currentMenuItemHasPopup(); + makeNoticesDismissible(); + aria_button_if_js(); + + $document.on( 'wp-pin-menu wp-window-resized.pin-menu postboxes-columnchange.pin-menu postbox-toggled.pin-menu wp-collapse-menu.pin-menu wp-scroll-start.pin-menu', setPinMenu ); + + // Set initial focus on a specific element. + $( '.wp-initial-focus' ).trigger( 'focus' ); + + // Toggle update details on update-core.php. + $body.on( 'click', '.js-update-details-toggle', function() { + var $updateNotice = $( this ).closest( '.js-update-details' ), + $progressDiv = $( '#' + $updateNotice.data( 'update-details' ) ); + + /* + * When clicking on "Show details" move the progress div below the update + * notice. Make sure it gets moved just the first time. + */ + if ( ! $progressDiv.hasClass( 'update-details-moved' ) ) { + $progressDiv.insertAfter( $updateNotice ).addClass( 'update-details-moved' ); + } + + // Toggle the progress div visibility. + $progressDiv.toggle(); + // Toggle the Show Details button expanded state. + $( this ).attr( 'aria-expanded', $progressDiv.is( ':visible' ) ); + }); +}); + +/** + * Hides the update button for expired plugin or theme uploads. + * + * On the "Update plugin/theme from uploaded zip" screen, once the upload has expired, + * hides the "Replace current with uploaded" button and displays a warning. + * + * @since 5.5.0 + */ +$( function( $ ) { + var $overwrite, $warning; + + if ( ! $body.hasClass( 'update-php' ) ) { + return; + } + + $overwrite = $( 'a.update-from-upload-overwrite' ); + $warning = $( '.update-from-upload-expired' ); + + if ( ! $overwrite.length || ! $warning.length ) { + return; + } + + window.setTimeout( + function() { + $overwrite.hide(); + $warning.removeClass( 'hidden' ); + + if ( window.wp && window.wp.a11y ) { + window.wp.a11y.speak( $warning.text() ); + } + }, + 7140000 // 119 minutes. The uploaded file is deleted after 2 hours. + ); +} ); + +// Fire a custom jQuery event at the end of window resize. +( function() { + var timeout; + + /** + * Triggers the WP window-resize event. + * + * @since 3.8.0 + * + * @return {void} + */ + function triggerEvent() { + $document.trigger( 'wp-window-resized' ); + } + + /** + * Fires the trigger event again after 200 ms. + * + * @since 3.8.0 + * + * @return {void} + */ + function fireOnce() { + window.clearTimeout( timeout ); + timeout = window.setTimeout( triggerEvent, 200 ); + } + + $window.on( 'resize.wp-fire-once', fireOnce ); +}()); + +// Make Windows 8 devices play along nicely. +(function(){ + if ( '-ms-user-select' in document.documentElement.style && navigator.userAgent.match(/IEMobile\/10\.0/) ) { + var msViewportStyle = document.createElement( 'style' ); + msViewportStyle.appendChild( + document.createTextNode( '@-ms-viewport{width:auto!important}' ) + ); + document.getElementsByTagName( 'head' )[0].appendChild( msViewportStyle ); + } +})(); + +}( jQuery, window )); + +/** + * Freeze animated plugin icons when reduced motion is enabled. + * + * When the user has enabled the 'prefers-reduced-motion' setting, this module + * stops animations for all GIFs on the page with the class 'plugin-icon' or + * plugin icon images in the update plugins table. + * + * @since 6.4.0 + */ +(function() { + // Private variables and methods. + var priv = {}, + pub = {}, + mediaQuery; + + // Initialize pauseAll to false; it will be set to true if reduced motion is preferred. + priv.pauseAll = false; + if ( window.matchMedia ) { + mediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); + if ( ! mediaQuery || mediaQuery.matches ) { + priv.pauseAll = true; + } + } + + // Method to replace animated GIFs with a static frame. + priv.freezeAnimatedPluginIcons = function( img ) { + var coverImage = function() { + var width = img.width; + var height = img.height; + var canvas = document.createElement( 'canvas' ); + + // Set canvas dimensions. + canvas.width = width; + canvas.height = height; + + // Copy classes from the image to the canvas. + canvas.className = img.className; + + // Check if the image is inside a specific table. + var isInsideUpdateTable = img.closest( '#update-plugins-table' ); + + if ( isInsideUpdateTable ) { + // Transfer computed styles from image to canvas. + var computedStyles = window.getComputedStyle( img ), + i, max; + for ( i = 0, max = computedStyles.length; i < max; i++ ) { + var propName = computedStyles[ i ]; + var propValue = computedStyles.getPropertyValue( propName ); + canvas.style[ propName ] = propValue; + } + } + + // Draw the image onto the canvas. + canvas.getContext( '2d' ).drawImage( img, 0, 0, width, height ); + + // Set accessibility attributes on canvas. + canvas.setAttribute( 'aria-hidden', 'true' ); + canvas.setAttribute( 'role', 'presentation' ); + + // Insert canvas before the image and set the image to be near-invisible. + var parent = img.parentNode; + parent.insertBefore( canvas, img ); + img.style.opacity = 0.01; + img.style.width = '0px'; + img.style.height = '0px'; + }; + + // If the image is already loaded, apply the coverImage function. + if ( img.complete ) { + coverImage(); + } else { + // Otherwise, wait for the image to load. + img.addEventListener( 'load', coverImage, true ); + } + }; + + // Public method to freeze all relevant GIFs on the page. + pub.freezeAll = function() { + var images = document.querySelectorAll( '.plugin-icon, #update-plugins-table img' ); + for ( var x = 0; x < images.length; x++ ) { + if ( /\.gif(?:\?|$)/i.test( images[ x ].src ) ) { + priv.freezeAnimatedPluginIcons( images[ x ] ); + } + } + }; + + // Only run the freezeAll method if the user prefers reduced motion. + if ( true === priv.pauseAll ) { + pub.freezeAll(); + } + + // Listen for jQuery AJAX events. + ( function( $ ) { + if ( window.pagenow === 'plugin-install' ) { + // Only listen for ajaxComplete if this is the plugin-install.php page. + $( document ).ajaxComplete( function( event, xhr, settings ) { + + // Check if this is the 'search-install-plugins' request. + if ( settings.data && typeof settings.data === 'string' && settings.data.includes( 'action=search-install-plugins' ) ) { + // Recheck if the user prefers reduced motion. + if ( window.matchMedia ) { + var mediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); + if ( mediaQuery.matches ) { + pub.freezeAll(); + } + } else { + // Fallback for browsers that don't support matchMedia. + if ( true === priv.pauseAll ) { + pub.freezeAll(); + } + } + } + } ); + } + } )( jQuery ); + + // Expose public methods. + return pub; +})(); diff --git a/wp-admin/js/common.min.js b/wp-admin/js/common.min.js new file mode 100644 index 0000000..200bce1 --- /dev/null +++ b/wp-admin/js/common.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(W,$){var Q=W(document),V=W($),q=W(document.body),H=wp.i18n.__,i=wp.i18n.sprintf;function r(e,t,n){n=void 0!==n?i(H("%1$s is deprecated since version %2$s! Use %3$s instead."),e,t,n):i(H("%1$s is deprecated since version %2$s with no alternative available."),e,t);$.console.warn(n)}function e(i,o,a){var s={};return Object.keys(o).forEach(function(e){var t=o[e],n=i+"."+e;"object"==typeof t?Object.defineProperty(s,e,{get:function(){return r(n,a,t.alternative),t.func()}}):Object.defineProperty(s,e,{get:function(){return r(n,a,"wp.i18n"),t}})}),s}$.wp.deprecateL10nObject=e,$.commonL10n=$.commonL10n||{warnDelete:"",dismiss:"",collapseMenu:"",expandMenu:""},$.commonL10n=e("commonL10n",$.commonL10n,"5.5.0"),$.wpPointerL10n=$.wpPointerL10n||{dismiss:""},$.wpPointerL10n=e("wpPointerL10n",$.wpPointerL10n,"5.5.0"),$.userProfileL10n=$.userProfileL10n||{warn:"",warnWeak:"",show:"",hide:"",cancel:"",ariaShow:"",ariaHide:""},$.userProfileL10n=e("userProfileL10n",$.userProfileL10n,"5.5.0"),$.privacyToolsL10n=$.privacyToolsL10n||{noDataFound:"",foundAndRemoved:"",noneRemoved:"",someNotRemoved:"",removalError:"",emailSent:"",noExportFile:"",exportError:""},$.privacyToolsL10n=e("privacyToolsL10n",$.privacyToolsL10n,"5.5.0"),$.authcheckL10n={beforeunload:""},$.authcheckL10n=$.authcheckL10n||e("authcheckL10n",$.authcheckL10n,"5.5.0"),$.tagsl10n={noPerm:"",broken:""},$.tagsl10n=$.tagsl10n||e("tagsl10n",$.tagsl10n,"5.5.0"),$.adminCommentsL10n=$.adminCommentsL10n||{hotkeys_highlight_first:{alternative:"window.adminCommentsSettings.hotkeys_highlight_first",func:function(){return $.adminCommentsSettings.hotkeys_highlight_first}},hotkeys_highlight_last:{alternative:"window.adminCommentsSettings.hotkeys_highlight_last",func:function(){return $.adminCommentsSettings.hotkeys_highlight_last}},replyApprove:"",reply:"",warnQuickEdit:"",warnCommentChanges:"",docTitleComments:"",docTitleCommentsCount:""},$.adminCommentsL10n=e("adminCommentsL10n",$.adminCommentsL10n,"5.5.0"),$.tagsSuggestL10n=$.tagsSuggestL10n||{tagDelimiter:"",removeTerm:"",termSelected:"",termAdded:"",termRemoved:""},$.tagsSuggestL10n=e("tagsSuggestL10n",$.tagsSuggestL10n,"5.5.0"),$.wpColorPickerL10n=$.wpColorPickerL10n||{clear:"",clearAriaLabel:"",defaultString:"",defaultAriaLabel:"",pick:"",defaultLabel:""},$.wpColorPickerL10n=e("wpColorPickerL10n",$.wpColorPickerL10n,"5.5.0"),$.attachMediaBoxL10n=$.attachMediaBoxL10n||{error:""},$.attachMediaBoxL10n=e("attachMediaBoxL10n",$.attachMediaBoxL10n,"5.5.0"),$.postL10n=$.postL10n||{ok:"",cancel:"",publishOn:"",publishOnFuture:"",publishOnPast:"",dateFormat:"",showcomm:"",endcomm:"",publish:"",schedule:"",update:"",savePending:"",saveDraft:"",private:"",public:"",publicSticky:"",password:"",privatelyPublished:"",published:"",saveAlert:"",savingText:"",permalinkSaved:""},$.postL10n=e("postL10n",$.postL10n,"5.5.0"),$.inlineEditL10n=$.inlineEditL10n||{error:"",ntdeltitle:"",notitle:"",comma:"",saved:""},$.inlineEditL10n=e("inlineEditL10n",$.inlineEditL10n,"5.5.0"),$.plugininstallL10n=$.plugininstallL10n||{plugin_information:"",plugin_modal_label:"",ays:""},$.plugininstallL10n=e("plugininstallL10n",$.plugininstallL10n,"5.5.0"),$.navMenuL10n=$.navMenuL10n||{noResultsFound:"",warnDeleteMenu:"",saveAlert:"",untitled:""},$.navMenuL10n=e("navMenuL10n",$.navMenuL10n,"5.5.0"),$.commentL10n=$.commentL10n||{submittedOn:"",dateFormat:""},$.commentL10n=e("commentL10n",$.commentL10n,"5.5.0"),$.setPostThumbnailL10n=$.setPostThumbnailL10n||{setThumbnail:"",saving:"",error:"",done:""},$.setPostThumbnailL10n=e("setPostThumbnailL10n",$.setPostThumbnailL10n,"5.5.0"),$.adminMenu={init:function(){},fold:function(){},restoreMenuState:function(){},toggle:function(){},favorites:function(){}},$.columns={init:function(){var n=this;W(".hide-column-tog","#adv-settings").on("click",function(){var e=W(this),t=e.val();e.prop("checked")?n.checked(t):n.unchecked(t),columns.saveManageColumnsState()})},saveManageColumnsState:function(){var e=this.hidden();W.post(ajaxurl,{action:"hidden-columns",hidden:e,screenoptionnonce:W("#screenoptionnonce").val(),page:pagenow})},checked:function(e){W(".column-"+e).removeClass("hidden"),this.colSpanChange(1)},unchecked:function(e){W(".column-"+e).addClass("hidden"),this.colSpanChange(-1)},hidden:function(){return W(".manage-column[id]").filter(".hidden").map(function(){return this.id}).get().join(",")},useCheckboxesForHidden:function(){this.hidden=function(){return W(".hide-column-tog").not(":checked").map(function(){var e=this.id;return e.substring(e,e.length-5)}).get().join(",")}},colSpanChange:function(e){var t=W("table").find(".colspanchange");t.length&&(e=parseInt(t.attr("colspan"),10)+e,t.attr("colspan",e.toString()))}},W(function(){columns.init()}),$.validateForm=function(e){return!W(e).find(".form-required").filter(function(){return""===W(":input:visible",this).val()}).addClass("form-invalid").find(":input:visible").on("change",function(){W(this).closest(".form-invalid").removeClass("form-invalid")}).length},$.showNotice={warn:function(){return!!confirm(H("You are about to permanently delete these items from your site.\nThis action cannot be undone.\n'Cancel' to stop, 'OK' to delete."))},note:function(e){alert(e)}},$.screenMeta={element:null,toggles:null,page:null,init:function(){this.element=W("#screen-meta"),this.toggles=W("#screen-meta-links").find(".show-settings"),this.page=W("#wpcontent"),this.toggles.on("click",this.toggleEvent)},toggleEvent:function(){var e=W("#"+W(this).attr("aria-controls"));e.length&&(e.is(":visible")?screenMeta.close(e,W(this)):screenMeta.open(e,W(this)))},open:function(e,t){W("#screen-meta-links").find(".screen-meta-toggle").not(t.parent()).css("visibility","hidden"),e.parent().show(),e.slideDown("fast",function(){e.removeClass("hidden").trigger("focus"),t.addClass("screen-meta-active").attr("aria-expanded",!0)}),Q.trigger("screen:options:open")},close:function(e,t){e.slideUp("fast",function(){t.removeClass("screen-meta-active").attr("aria-expanded",!1),W(".screen-meta-toggle").css("visibility",""),e.parent().hide(),e.addClass("hidden")}),Q.trigger("screen:options:close")}},W(".contextual-help-tabs").on("click","a",function(e){var t=W(this);if(e.preventDefault(),t.is(".active a"))return!1;W(".contextual-help-tabs .active").removeClass("active"),t.parent("li").addClass("active"),e=W(t.attr("href")),W(".help-tab-content").not(e).removeClass("active").hide(),e.addClass("active").show()});var t,a=!1,s=W("#permalink_structure"),n=W(".permalink-structure input:radio"),l=W("#custom_selection"),o=W(".form-table.permalink-structure .available-structure-tags button");function c(e){-1!==s.val().indexOf(e.text().trim())?(e.attr("data-label",e.attr("aria-label")),e.attr("aria-label",e.attr("data-used")),e.attr("aria-pressed",!0),e.addClass("active")):e.attr("data-label")&&(e.attr("aria-label",e.attr("data-label")),e.attr("aria-pressed",!1),e.removeClass("active"))}function d(){Q.trigger("wp-window-resized")}n.on("change",function(){"custom"!==this.value&&(s.val(this.value),o.each(function(){c(W(this))}))}),s.on("click input",function(){l.prop("checked",!0)}),s.on("focus",function(e){a=!0,W(this).off(e)}),o.each(function(){c(W(this))}),s.on("change",function(){o.each(function(){c(W(this))})}),o.on("click",function(){var e=s.val(),t=s[0].selectionStart,n=s[0].selectionEnd,i=W(this).text().trim(),o=W(this).hasClass("active")?W(this).attr("data-removed"):W(this).attr("data-added");-1!==e.indexOf(i)?(e=e.replace(i+"/",""),s.val("/"===e?"":e),W("#custom_selection_updated").text(o),c(W(this))):(a||0!==t||0!==n||(t=n=e.length),l.prop("checked",!0),"/"!==e.substr(0,t).substr(-1)&&(i="/"+i),"/"!==e.substr(n,1)&&(i+="/"),s.val(e.substr(0,t)+i+e.substr(n)),W("#custom_selection_updated").text(o),c(W(this)),a&&s[0].setSelectionRange&&(n=(e.substr(0,t)+i).length,s[0].setSelectionRange(n,n),s.trigger("focus")))}),W(function(){var n,i,o,a,e,t,s,r,l,c,d=!1,u=W("input.current-page"),z=u.val(),p=/iPhone|iPad|iPod/.test(navigator.userAgent),N=-1!==navigator.userAgent.indexOf("Android"),m=W("#adminmenuwrap"),h=W("#wpwrap"),f=W("#adminmenu"),g=W("#wp-responsive-overlay"),v=W("#wp-toolbar"),b=v.find('a[aria-haspopup="true"]'),w=W(".meta-box-sortables"),k=!1,C=W("#wpadminbar"),y=0,L=!1,x=!1,S=0,P=!1,T={window:V.height(),wpwrap:h.height(),adminbar:C.height(),menu:m.height()},A=W(".wp-header-end");function M(){var e=W("a.wp-has-current-submenu");"folded"===s?e.attr("aria-haspopup","true"):e.attr("aria-haspopup","false")}function _(e){var t=e.find(".wp-submenu"),e=e.offset().top,n=V.scrollTop(),i=e-n-30,e=e+t.height()+1,o=60+e-h.height(),n=V.height()+n-50;1<(o=i<(o=n<e-o?e-n:o)?i:o)&&W("#wp-admin-bar-menu-toggle").is(":hidden")?t.css("margin-top","-"+o+"px"):t.css("margin-top","")}function D(){W(".notice.is-dismissible").each(function(){var t=W(this),e=W('<button type="button" class="notice-dismiss"><span class="screen-reader-text"></span></button>');t.find(".notice-dismiss").length||(e.find(".screen-reader-text").text(H("Dismiss this notice.")),e.on("click.wp-dismiss-notice",function(e){e.preventDefault(),t.fadeTo(100,0,function(){t.slideUp(100,function(){t.remove()})})}),t.append(e))})}function E(e,t,n,i){n.on("change",function(){e.val(W(this).val())}),e.on("change",function(){n.val(W(this).val())}),i.on("click",function(e){e.preventDefault(),e.stopPropagation(),t.trigger("click")})}function R(){r.prop("disabled",""===l.map(function(){return W(this).val()}).get().join(""))}function F(e){var t=V.scrollTop(),e=!e||"scroll"!==e.type;if(!p&&!f.data("wp-responsive"))if(T.menu+T.adminbar<T.window||T.menu+T.adminbar+20>T.wpwrap)j();else{if(P=!0,T.menu+T.adminbar>T.window){if(t<0)return void(L||(x=!(L=!0),m.css({position:"fixed",top:"",bottom:""})));if(t+T.window>Q.height()-1)return void(x||(L=!(x=!0),m.css({position:"fixed",top:"",bottom:0})));y<t?L?(L=!1,(S=m.offset().top-T.adminbar-(t-y))+T.menu+T.adminbar<t+T.window&&(S=t+T.window-T.menu-T.adminbar),m.css({position:"absolute",top:S,bottom:""})):!x&&m.offset().top+T.menu<t+T.window&&(x=!0,m.css({position:"fixed",top:"",bottom:0})):t<y?x?(x=!1,(S=m.offset().top-T.adminbar+(y-t))+T.menu>t+T.window&&(S=t),m.css({position:"absolute",top:S,bottom:""})):!L&&m.offset().top>=t+T.adminbar&&(L=!0,m.css({position:"fixed",top:"",bottom:""})):e&&(L=x=!1,0<(S=t+T.window-T.menu-T.adminbar-1)?m.css({position:"absolute",top:S,bottom:""}):j())}y=t}}function U(){T={window:V.height(),wpwrap:h.height(),adminbar:C.height(),menu:m.height()}}function j(){!p&&P&&(L=x=P=!1,m.css({position:"",top:"",bottom:""}))}function O(){U(),f.data("wp-responsive")?(q.removeClass("sticky-menu"),j()):T.menu+T.adminbar>T.window?(F(),q.removeClass("sticky-menu")):(q.addClass("sticky-menu"),j())}function K(){W(".aria-button-if-js").attr("role","button")}function I(){var e=!1;return e=$.innerWidth?Math.max($.innerWidth,document.documentElement.clientWidth):e}function B(){var e=I()||961;s=e<=782?"responsive":q.hasClass("folded")||q.hasClass("auto-fold")&&e<=960&&782<e?"folded":"open",Q.trigger("wp-menu-state-set",{state:s})}f.on("click.wp-submenu-head",".wp-submenu-head",function(e){W(e.target).parent().siblings("a").get(0).click()}),W("#collapse-button").on("click.collapse-menu",function(){var e=I()||961;W("#adminmenu div.wp-submenu").css("margin-top",""),s=e<=960?q.hasClass("auto-fold")?(q.removeClass("auto-fold").removeClass("folded"),setUserSetting("unfold",1),setUserSetting("mfold","o"),"open"):(q.addClass("auto-fold"),setUserSetting("unfold",0),"folded"):q.hasClass("folded")?(q.removeClass("folded"),setUserSetting("mfold","o"),"open"):(q.addClass("folded"),setUserSetting("mfold","f"),"folded"),Q.trigger("wp-collapse-menu",{state:s})}),Q.on("wp-menu-state-set wp-collapse-menu wp-responsive-activate wp-responsive-deactivate",M),("ontouchstart"in $||/IEMobile\/[1-9]/.test(navigator.userAgent))&&(q.on((c=p?"touchstart":"click")+".wp-mobile-hover",function(e){f.data("wp-responsive")||W(e.target).closest("#adminmenu").length||f.find("li.opensub").removeClass("opensub")}),f.find("a.wp-has-submenu").on(c+".wp-mobile-hover",function(e){var t=W(this).parent();f.data("wp-responsive")||t.hasClass("opensub")||t.hasClass("wp-menu-open")&&!(t.width()<40)||(e.preventDefault(),_(t),f.find("li.opensub").removeClass("opensub"),t.addClass("opensub"))})),p||N||(f.find("li.wp-has-submenu").hoverIntent({over:function(){var e=W(this),t=e.find(".wp-submenu"),t=parseInt(t.css("top"),10);isNaN(t)||-5<t||f.data("wp-responsive")||(_(e),f.find("li.opensub").removeClass("opensub"),e.addClass("opensub"))},out:function(){f.data("wp-responsive")||W(this).removeClass("opensub").find(".wp-submenu").css("margin-top","")},timeout:200,sensitivity:7,interval:90}),f.on("focus.adminmenu",".wp-submenu a",function(e){f.data("wp-responsive")||W(e.target).closest("li.menu-top").addClass("opensub")}).on("blur.adminmenu",".wp-submenu a",function(e){f.data("wp-responsive")||W(e.target).closest("li.menu-top").removeClass("opensub")}).find("li.wp-has-submenu.wp-not-current-submenu").on("focusin.adminmenu",function(){_(W(this))})),A.length||(A=W(".wrap h1, .wrap h2").first()),W("div.updated, div.error, div.notice").not(".inline, .below-h2").insertAfter(A),Q.on("wp-updates-notice-added wp-plugin-install-error wp-plugin-update-error wp-plugin-delete-error wp-theme-install-error wp-theme-delete-error",D),screenMeta.init(),q.on("click","tbody > tr > .check-column :checkbox",function(e){if("undefined"!=e.shiftKey){if(e.shiftKey){if(!d)return!0;n=W(d).closest("form").find(":checkbox").filter(":visible:enabled"),i=n.index(d),o=n.index(this),a=W(this).prop("checked"),0<i&&0<o&&i!=o&&(i<o?n.slice(i,o):n.slice(o,i)).prop("checked",function(){return!!W(this).closest("tr").is(":visible")&&a})}var t=W(d=this).closest("tbody").find(":checkbox").filter(":visible:enabled").not(":checked");W(this).closest("table").children("thead, tfoot").find(":checkbox").prop("checked",function(){return 0===t.length})}return!0}),q.on("click.wp-toggle-checkboxes","thead .check-column :checkbox, tfoot .check-column :checkbox",function(e){var t=W(this),n=t.closest("table"),i=t.prop("checked"),o=e.shiftKey||t.data("wp-toggle");n.children("tbody").filter(":visible").children().children(".check-column").find(":checkbox").prop("checked",function(){return!W(this).is(":hidden,:disabled")&&(o?!W(this).prop("checked"):!!i)}),n.children("thead, tfoot").filter(":visible").children().children(".check-column").find(":checkbox").prop("checked",function(){return!o&&!!i})}),E(W("#bulk-action-selector-top"),W("#doaction"),W("#bulk-action-selector-bottom"),W("#doaction2")),E(W("#new_role"),W("#changeit"),W("#new_role2"),W("#changeit2")),W("#wpbody-content").on({focusin:function(){clearTimeout(e),t=W(this).find(".row-actions"),W(".row-actions").not(this).removeClass("visible"),t.addClass("visible")},focusout:function(){e=setTimeout(function(){t.removeClass("visible")},30)}},".table-view-list .has-row-actions"),W("tbody").on("click",".toggle-row",function(){W(this).closest("tr").toggleClass("is-expanded")}),W("#default-password-nag-no").on("click",function(){return setUserSetting("default_password_nag","hide"),W("div.default-password-nag").hide(),!1}),W("#newcontent").on("keydown.wpevent_InsertTab",function(e){var t,n,i,o,a=e.target;27==e.keyCode?(e.preventDefault(),W(a).data("tab-out",!0)):9!=e.keyCode||e.ctrlKey||e.altKey||e.shiftKey||(W(a).data("tab-out")?W(a).data("tab-out",!1):(t=a.selectionStart,n=a.selectionEnd,i=a.value,document.selection?(a.focus(),document.selection.createRange().text="\t"):0<=t&&(o=this.scrollTop,a.value=i.substring(0,t).concat("\t",i.substring(n)),a.selectionStart=a.selectionEnd=t+1,this.scrollTop=o),e.stopPropagation&&e.stopPropagation(),e.preventDefault&&e.preventDefault()))}),u.length&&u.closest("form").on("submit",function(){-1==W('select[name="action"]').val()&&u.val()==z&&u.val("1")}),W('.search-box input[type="search"], .search-box input[type="submit"]').on("mousedown",function(){W('select[name^="action"]').val("-1")}),W("#contextual-help-link, #show-settings-link").on("focus.scroll-into-view",function(e){e.target.scrollIntoViewIfNeeded&&e.target.scrollIntoViewIfNeeded(!1)}),(c=W("form.wp-upload-form")).length&&(r=c.find('input[type="submit"]'),l=c.find('input[type="file"]'),R(),l.on("change",R)),p||(V.on("scroll.pin-menu",F),Q.on("tinymce-editor-init.pin-menu",function(e,t){t.on("wp-autoresize",U)})),$.wpResponsive={init:function(){var e=this;this.maybeDisableSortables=this.maybeDisableSortables.bind(this),Q.on("wp-responsive-activate.wp-responsive",function(){e.activate()}).on("wp-responsive-deactivate.wp-responsive",function(){e.deactivate()}),W("#wp-admin-bar-menu-toggle a").attr("aria-expanded","false"),W("#wp-admin-bar-menu-toggle").on("click.wp-responsive",function(e){e.preventDefault(),C.find(".hover").removeClass("hover"),h.toggleClass("wp-responsive-open"),h.hasClass("wp-responsive-open")?(W(this).find("a").attr("aria-expanded","true"),W("#adminmenu a:first").trigger("focus")):W(this).find("a").attr("aria-expanded","false")}),W(document).on("click",function(e){var t;h.hasClass("wp-responsive-open")&&document.hasFocus()&&(t=W.contains(W("#wp-admin-bar-menu-toggle")[0],e.target),e=W.contains(W("#adminmenuwrap")[0],e.target),t||e||W("#wp-admin-bar-menu-toggle").trigger("click.wp-responsive"))}),W(document).on("keyup",function(e){var n,i,o=W("#wp-admin-bar-menu-toggle")[0];h.hasClass("wp-responsive-open")&&(27===e.keyCode?(W(o).trigger("click.wp-responsive"),W(o).find("a").trigger("focus")):9===e.keyCode&&(n=W("#adminmenuwrap")[0],i=e.relatedTarget||document.activeElement,setTimeout(function(){var e=W.contains(o,i),t=W.contains(n,i);e||t||W(o).trigger("click.wp-responsive")},10)))}),f.on("click.wp-responsive","li.wp-has-submenu > a",function(e){f.data("wp-responsive")&&(W(this).parent("li").toggleClass("selected"),W(this).trigger("focus"),e.preventDefault())}),e.trigger(),Q.on("wp-window-resized.wp-responsive",this.trigger.bind(this)),V.on("load.wp-responsive",this.maybeDisableSortables),Q.on("postbox-toggled",this.maybeDisableSortables),W("#screen-options-wrap input").on("click",this.maybeDisableSortables)},maybeDisableSortables:function(){(-1<navigator.userAgent.indexOf("AppleWebKit/")?V.width():$.innerWidth)<=782||w.find(".ui-sortable-handle:visible").length<=1&&jQuery(".columns-prefs-1 input").prop("checked")?this.disableSortables():this.enableSortables()},activate:function(){O(),q.hasClass("auto-fold")||q.addClass("auto-fold"),f.data("wp-responsive",1),this.disableSortables()},deactivate:function(){O(),f.removeData("wp-responsive"),this.maybeDisableSortables()},trigger:function(){var e=I();e&&(e<=782?k||(Q.trigger("wp-responsive-activate"),k=!0):k&&(Q.trigger("wp-responsive-deactivate"),k=!1),e<=480?this.enableOverlay():this.disableOverlay(),this.maybeDisableSortables())},enableOverlay:function(){0===g.length&&(g=W('<div id="wp-responsive-overlay"></div>').insertAfter("#wpcontent").hide().on("click.wp-responsive",function(){v.find(".menupop.hover").removeClass("hover"),W(this).hide()})),b.on("click.wp-responsive",function(){g.show()})},disableOverlay:function(){b.off("click.wp-responsive"),g.hide()},disableSortables:function(){if(w.length)try{w.sortable("disable"),w.find(".ui-sortable-handle").addClass("is-non-sortable")}catch(e){}},enableSortables:function(){if(w.length)try{w.sortable("enable"),w.find(".ui-sortable-handle").removeClass("is-non-sortable")}catch(e){}}},W(document).on("ajaxComplete",function(){K()}),Q.on("wp-window-resized.set-menu-state",B),Q.on("wp-menu-state-set wp-collapse-menu",function(e,t){var n,i=W("#collapse-button"),t="folded"===t.state?(n="false",H("Expand Main menu")):(n="true",H("Collapse Main menu"));i.attr({"aria-expanded":n,"aria-label":t})}),$.wpResponsive.init(),O(),B(),M(),D(),K(),Q.on("wp-pin-menu wp-window-resized.pin-menu postboxes-columnchange.pin-menu postbox-toggled.pin-menu wp-collapse-menu.pin-menu wp-scroll-start.pin-menu",O),W(".wp-initial-focus").trigger("focus"),q.on("click",".js-update-details-toggle",function(){var e=W(this).closest(".js-update-details"),t=W("#"+e.data("update-details"));t.hasClass("update-details-moved")||t.insertAfter(e).addClass("update-details-moved"),t.toggle(),W(this).attr("aria-expanded",t.is(":visible"))})}),W(function(e){var t,n;q.hasClass("update-php")&&(t=e("a.update-from-upload-overwrite"),n=e(".update-from-upload-expired"),t.length)&&n.length&&$.setTimeout(function(){t.hide(),n.removeClass("hidden"),$.wp&&$.wp.a11y&&$.wp.a11y.speak(n.text())},714e4)}),V.on("resize.wp-fire-once",function(){$.clearTimeout(t),t=$.setTimeout(d,200)}),"-ms-user-select"in document.documentElement.style&&navigator.userAgent.match(/IEMobile\/10\.0/)&&((n=document.createElement("style")).appendChild(document.createTextNode("@-ms-viewport{width:auto!important}")),document.getElementsByTagName("head")[0].appendChild(n))}(jQuery,window),function(){var e,i={},o={};i.pauseAll=!1,!window.matchMedia||(e=window.matchMedia("(prefers-reduced-motion: reduce)"))&&!e.matches||(i.pauseAll=!0),i.freezeAnimatedPluginIcons=function(l){function e(){var e=l.width,t=l.height,n=document.createElement("canvas");if(n.width=e,n.height=t,n.className=l.className,l.closest("#update-plugins-table"))for(var i=window.getComputedStyle(l),o=0,a=i.length;o<a;o++){var s=i[o],r=i.getPropertyValue(s);n.style[s]=r}n.getContext("2d").drawImage(l,0,0,e,t),n.setAttribute("aria-hidden","true"),n.setAttribute("role","presentation"),l.parentNode.insertBefore(n,l),l.style.opacity=.01,l.style.width="0px",l.style.height="0px"}l.complete?e():l.addEventListener("load",e,!0)},o.freezeAll=function(){for(var e=document.querySelectorAll(".plugin-icon, #update-plugins-table img"),t=0;t<e.length;t++)/\.gif(?:\?|$)/i.test(e[t].src)&&i.freezeAnimatedPluginIcons(e[t])},!0===i.pauseAll&&o.freezeAll(),e=jQuery,"plugin-install"===window.pagenow&&e(document).ajaxComplete(function(e,t,n){n.data&&"string"==typeof n.data&&n.data.includes("action=search-install-plugins")&&(window.matchMedia?window.matchMedia("(prefers-reduced-motion: reduce)").matches&&o.freezeAll():!0===i.pauseAll&&o.freezeAll())})}();
\ No newline at end of file diff --git a/wp-admin/js/custom-background.js b/wp-admin/js/custom-background.js new file mode 100644 index 0000000..f83db00 --- /dev/null +++ b/wp-admin/js/custom-background.js @@ -0,0 +1,147 @@ +/** + * @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); diff --git a/wp-admin/js/custom-background.min.js b/wp-admin/js/custom-background.min.js new file mode 100644 index 0000000..6ad9fa5 --- /dev/null +++ b/wp-admin/js/custom-background.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(e){e(function(){var c,a=e("#custom-background-image");e("#background-color").wpColorPicker({change:function(n,o){a.css("background-color",o.color.toString())},clear:function(){a.css("background-color","")}}),e('select[name="background-size"]').on("change",function(){a.css("background-size",e(this).val())}),e('input[name="background-position"]').on("change",function(){a.css("background-position",e(this).val())}),e('input[name="background-repeat"]').on("change",function(){a.css("background-repeat",e(this).is(":checked")?"repeat":"no-repeat")}),e('input[name="background-attachment"]').on("change",function(){a.css("background-attachment",e(this).is(":checked")?"scroll":"fixed")}),e("#choose-from-library-link").on("click",function(n){var o=e(this);n.preventDefault(),c||(c=wp.media.frames.customBackground=wp.media({title:o.data("choose"),library:{type:"image"},button:{text:o.data("update"),close:!1}})).on("select",function(){var n=c.state().get("selection").first(),o=e("#_wpnonce").val()||"";e.post(ajaxurl,{action:"set-background-image",attachment_id:n.id,_ajax_nonce:o,size:"full"}).done(function(){window.location.reload()})}),c.open()})})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/custom-header.js b/wp-admin/js/custom-header.js new file mode 100644 index 0000000..367756e --- /dev/null +++ b/wp-admin/js/custom-header.js @@ -0,0 +1,88 @@ +/** + * @output wp-admin/js/custom-header.js + */ + +/* global isRtl */ + +/** + * Initializes the custom header selection page. + * + * @since 3.5.0 + * + * @deprecated 4.1.0 The page this is used on is never linked to from the UI. + * Setting a custom header is completely handled by the Customizer. + */ +(function($) { + var frame; + + $( function() { + // Fetch available headers. + var $headers = $('.available-headers'); + + // Apply jQuery.masonry once the images have loaded. + $headers.imagesLoaded( function() { + $headers.masonry({ + itemSelector: '.default-header', + isRTL: !! ( 'undefined' != typeof isRtl && isRtl ) + }); + }); + + /** + * Opens the 'choose from library' frame and creates it if it doesn't exist. + * + * @since 3.5.0 + * @deprecated 4.1.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.customHeader = 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 + } + }); + + /** + * Updates the window location to include the selected attachment. + * + * @since 3.5.0 + * @deprecated 4.1.0 + * + * @return {void} + */ + frame.on( 'select', function() { + // Grab the selected attachment. + var attachment = frame.state().get('selection').first(), + link = $el.data('updateLink'); + + // Tell the browser to navigate to the crop step. + window.location = link + '&file=' + attachment.id; + }); + + frame.open(); + }); + }); +}(jQuery)); diff --git a/wp-admin/js/customize-controls.js b/wp-admin/js/customize-controls.js new file mode 100644 index 0000000..b6786b4 --- /dev/null +++ b/wp-admin/js/customize-controls.js @@ -0,0 +1,9353 @@ +/** + * @output wp-admin/js/customize-controls.js + */ + +/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */ +(function( exports, $ ){ + var Container, focus, normalizedTransitionendEventName, api = wp.customize; + + var reducedMotionMediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); + var isReducedMotion = reducedMotionMediaQuery.matches; + reducedMotionMediaQuery.addEventListener( 'change' , function handleReducedMotionChange( event ) { + isReducedMotion = event.matches; + }); + + api.OverlayNotification = api.Notification.extend(/** @lends wp.customize.OverlayNotification.prototype */{ + + /** + * Whether the notification should show a loading spinner. + * + * @since 4.9.0 + * @var {boolean} + */ + loading: false, + + /** + * A notification that is displayed in a full-screen overlay. + * + * @constructs wp.customize.OverlayNotification + * @augments wp.customize.Notification + * + * @since 4.9.0 + * + * @param {string} code - Code. + * @param {Object} params - Params. + */ + initialize: function( code, params ) { + var notification = this; + api.Notification.prototype.initialize.call( notification, code, params ); + notification.containerClasses += ' notification-overlay'; + if ( notification.loading ) { + notification.containerClasses += ' notification-loading'; + } + }, + + /** + * Render notification. + * + * @since 4.9.0 + * + * @return {jQuery} Notification container. + */ + render: function() { + var li = api.Notification.prototype.render.call( this ); + li.on( 'keydown', _.bind( this.handleEscape, this ) ); + return li; + }, + + /** + * Stop propagation on escape key presses, but also dismiss notification if it is dismissible. + * + * @since 4.9.0 + * + * @param {jQuery.Event} event - Event. + * @return {void} + */ + handleEscape: function( event ) { + var notification = this; + if ( 27 === event.which ) { + event.stopPropagation(); + if ( notification.dismissible && notification.parent ) { + notification.parent.remove( notification.code ); + } + } + } + }); + + api.Notifications = api.Values.extend(/** @lends wp.customize.Notifications.prototype */{ + + /** + * Whether the alternative style should be used. + * + * @since 4.9.0 + * @type {boolean} + */ + alt: false, + + /** + * The default constructor for items of the collection. + * + * @since 4.9.0 + * @type {object} + */ + defaultConstructor: api.Notification, + + /** + * A collection of observable notifications. + * + * @since 4.9.0 + * + * @constructs wp.customize.Notifications + * @augments wp.customize.Values + * + * @param {Object} options - Options. + * @param {jQuery} [options.container] - Container element for notifications. This can be injected later. + * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications. + * + * @return {void} + */ + initialize: function( options ) { + var collection = this; + + api.Values.prototype.initialize.call( collection, options ); + + _.bindAll( collection, 'constrainFocus' ); + + // Keep track of the order in which the notifications were added for sorting purposes. + collection._addedIncrement = 0; + collection._addedOrder = {}; + + // Trigger change event when notification is added or removed. + collection.bind( 'add', function( notification ) { + collection.trigger( 'change', notification ); + }); + collection.bind( 'removed', function( notification ) { + collection.trigger( 'change', notification ); + }); + }, + + /** + * Get the number of notifications added. + * + * @since 4.9.0 + * @return {number} Count of notifications. + */ + count: function() { + return _.size( this._value ); + }, + + /** + * Add notification to the collection. + * + * @since 4.9.0 + * + * @param {string|wp.customize.Notification} notification - Notification object to add. Alternatively code may be supplied, and in that case the second notificationObject argument must be supplied. + * @param {wp.customize.Notification} [notificationObject] - Notification to add when first argument is the code string. + * @return {wp.customize.Notification} Added notification (or existing instance if it was already added). + */ + add: function( notification, notificationObject ) { + var collection = this, code, instance; + if ( 'string' === typeof notification ) { + code = notification; + instance = notificationObject; + } else { + code = notification.code; + instance = notification; + } + if ( ! collection.has( code ) ) { + collection._addedIncrement += 1; + collection._addedOrder[ code ] = collection._addedIncrement; + } + return api.Values.prototype.add.call( collection, code, instance ); + }, + + /** + * Add notification to the collection. + * + * @since 4.9.0 + * @param {string} code - Notification code to remove. + * @return {api.Notification} Added instance (or existing instance if it was already added). + */ + remove: function( code ) { + var collection = this; + delete collection._addedOrder[ code ]; + return api.Values.prototype.remove.call( this, code ); + }, + + /** + * Get list of notifications. + * + * Notifications may be sorted by type followed by added time. + * + * @since 4.9.0 + * @param {Object} args - Args. + * @param {boolean} [args.sort=false] - Whether to return the notifications sorted. + * @return {Array.<wp.customize.Notification>} Notifications. + */ + get: function( args ) { + var collection = this, notifications, errorTypePriorities, params; + notifications = _.values( collection._value ); + + params = _.extend( + { sort: false }, + args + ); + + if ( params.sort ) { + errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 }; + notifications.sort( function( a, b ) { + var aPriority = 0, bPriority = 0; + if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) { + aPriority = errorTypePriorities[ a.type ]; + } + if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) { + bPriority = errorTypePriorities[ b.type ]; + } + if ( aPriority !== bPriority ) { + return bPriority - aPriority; // Show errors first. + } + return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher. + }); + } + + return notifications; + }, + + /** + * Render notifications area. + * + * @since 4.9.0 + * @return {void} + */ + render: function() { + var collection = this, + notifications, hadOverlayNotification = false, hasOverlayNotification, overlayNotifications = [], + previousNotificationsByCode = {}, + listElement, focusableElements; + + // Short-circuit if there are no container to render into. + if ( ! collection.container || ! collection.container.length ) { + return; + } + + notifications = collection.get( { sort: true } ); + collection.container.toggle( 0 !== notifications.length ); + + // Short-circuit if there are no changes to the notifications. + if ( collection.container.is( collection.previousContainer ) && _.isEqual( notifications, collection.previousNotifications ) ) { + return; + } + + // Make sure list is part of the container. + listElement = collection.container.children( 'ul' ).first(); + if ( ! listElement.length ) { + listElement = $( '<ul></ul>' ); + collection.container.append( listElement ); + } + + // Remove all notifications prior to re-rendering. + listElement.find( '> [data-code]' ).remove(); + + _.each( collection.previousNotifications, function( notification ) { + previousNotificationsByCode[ notification.code ] = notification; + }); + + // Add all notifications in the sorted order. + _.each( notifications, function( notification ) { + var notificationContainer; + if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) { + wp.a11y.speak( notification.message, 'assertive' ); + } + notificationContainer = $( notification.render() ); + notification.container = notificationContainer; + listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement. + + if ( notification.extended( api.OverlayNotification ) ) { + overlayNotifications.push( notification ); + } + }); + hasOverlayNotification = Boolean( overlayNotifications.length ); + + if ( collection.previousNotifications ) { + hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) { + return notification.extended( api.OverlayNotification ); + } ) ); + } + + if ( hasOverlayNotification !== hadOverlayNotification ) { + $( document.body ).toggleClass( 'customize-loading', hasOverlayNotification ); + collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification ); + if ( hasOverlayNotification ) { + collection.previousActiveElement = document.activeElement; + $( document ).on( 'keydown', collection.constrainFocus ); + } else { + $( document ).off( 'keydown', collection.constrainFocus ); + } + } + + if ( hasOverlayNotification ) { + collection.focusContainer = overlayNotifications[ overlayNotifications.length - 1 ].container; + collection.focusContainer.prop( 'tabIndex', -1 ); + focusableElements = collection.focusContainer.find( ':focusable' ); + if ( focusableElements.length ) { + focusableElements.first().focus(); + } else { + collection.focusContainer.focus(); + } + } else if ( collection.previousActiveElement ) { + $( collection.previousActiveElement ).trigger( 'focus' ); + collection.previousActiveElement = null; + } + + collection.previousNotifications = notifications; + collection.previousContainer = collection.container; + collection.trigger( 'rendered' ); + }, + + /** + * Constrain focus on focus container. + * + * @since 4.9.0 + * + * @param {jQuery.Event} event - Event. + * @return {void} + */ + constrainFocus: function constrainFocus( event ) { + var collection = this, focusableElements; + + // Prevent keys from escaping. + event.stopPropagation(); + + if ( 9 !== event.which ) { // Tab key. + return; + } + + focusableElements = collection.focusContainer.find( ':focusable' ); + if ( 0 === focusableElements.length ) { + focusableElements = collection.focusContainer; + } + + if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) { + event.preventDefault(); + focusableElements.first().focus(); + } else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) { + event.preventDefault(); + focusableElements.first().focus(); + } else if ( focusableElements.first().is( event.target ) && event.shiftKey ) { + event.preventDefault(); + focusableElements.last().focus(); + } + } + }); + + api.Setting = api.Value.extend(/** @lends wp.customize.Setting.prototype */{ + + /** + * Default params. + * + * @since 4.9.0 + * @var {object} + */ + defaults: { + transport: 'refresh', + dirty: false + }, + + /** + * A Customizer Setting. + * + * A setting is WordPress data (theme mod, option, menu, etc.) that the user can + * draft changes to in the Customizer. + * + * @see PHP class WP_Customize_Setting. + * + * @constructs wp.customize.Setting + * @augments wp.customize.Value + * + * @since 3.4.0 + * + * @param {string} id - The setting ID. + * @param {*} value - The initial value of the setting. + * @param {Object} [options={}] - Options. + * @param {string} [options.transport=refresh] - The transport to use for previewing. Supports 'refresh' and 'postMessage'. + * @param {boolean} [options.dirty=false] - Whether the setting should be considered initially dirty. + * @param {Object} [options.previewer] - The Previewer instance to sync with. Defaults to wp.customize.previewer. + */ + initialize: function( id, value, options ) { + var setting = this, params; + params = _.extend( + { previewer: api.previewer }, + setting.defaults, + options || {} + ); + + api.Value.prototype.initialize.call( setting, value, params ); + + setting.id = id; + setting._dirty = params.dirty; // The _dirty property is what the Customizer reads from. + setting.notifications = new api.Notifications(); + + // Whenever the setting's value changes, refresh the preview. + setting.bind( setting.preview ); + }, + + /** + * Refresh the preview, respective of the setting's refresh policy. + * + * If the preview hasn't sent a keep-alive message and is likely + * disconnected by having navigated to a non-allowed URL, then the + * refresh transport will be forced when postMessage is the transport. + * Note that postMessage does not throw an error when the recipient window + * fails to match the origin window, so using try/catch around the + * previewer.send() call to then fallback to refresh will not work. + * + * @since 3.4.0 + * @access public + * + * @return {void} + */ + preview: function() { + var setting = this, transport; + transport = setting.transport; + + if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) { + transport = 'refresh'; + } + + if ( 'postMessage' === transport ) { + setting.previewer.send( 'setting', [ setting.id, setting() ] ); + } else if ( 'refresh' === transport ) { + setting.previewer.refresh(); + } + }, + + /** + * Find controls associated with this setting. + * + * @since 4.6.0 + * @return {wp.customize.Control[]} Controls associated with setting. + */ + findControls: function() { + var setting = this, controls = []; + api.control.each( function( control ) { + _.each( control.settings, function( controlSetting ) { + if ( controlSetting.id === setting.id ) { + controls.push( control ); + } + } ); + } ); + return controls; + } + }); + + /** + * Current change count. + * + * @alias wp.customize._latestRevision + * + * @since 4.7.0 + * @type {number} + * @protected + */ + api._latestRevision = 0; + + /** + * Last revision that was saved. + * + * @alias wp.customize._lastSavedRevision + * + * @since 4.7.0 + * @type {number} + * @protected + */ + api._lastSavedRevision = 0; + + /** + * Latest revisions associated with the updated setting. + * + * @alias wp.customize._latestSettingRevisions + * + * @since 4.7.0 + * @type {object} + * @protected + */ + api._latestSettingRevisions = {}; + + /* + * Keep track of the revision associated with each updated setting so that + * requestChangesetUpdate knows which dirty settings to include. Also, once + * ready is triggered and all initial settings have been added, increment + * revision for each newly-created initially-dirty setting so that it will + * also be included in changeset update requests. + */ + api.bind( 'change', function incrementChangedSettingRevision( setting ) { + api._latestRevision += 1; + api._latestSettingRevisions[ setting.id ] = api._latestRevision; + } ); + api.bind( 'ready', function() { + api.bind( 'add', function incrementCreatedSettingRevision( setting ) { + if ( setting._dirty ) { + api._latestRevision += 1; + api._latestSettingRevisions[ setting.id ] = api._latestRevision; + } + } ); + } ); + + /** + * Get the dirty setting values. + * + * @alias wp.customize.dirtyValues + * + * @since 4.7.0 + * @access public + * + * @param {Object} [options] Options. + * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes). + * @return {Object} Dirty setting values. + */ + api.dirtyValues = function dirtyValues( options ) { + var values = {}; + api.each( function( setting ) { + var settingRevision; + + if ( ! setting._dirty ) { + return; + } + + settingRevision = api._latestSettingRevisions[ setting.id ]; + + // Skip including settings that have already been included in the changeset, if only requesting unsaved. + if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) { + return; + } + + values[ setting.id ] = setting.get(); + } ); + return values; + }; + + /** + * Request updates to the changeset. + * + * @alias wp.customize.requestChangesetUpdate + * + * @since 4.7.0 + * @access public + * + * @param {Object} [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null. + * If not provided, then the changes will still be obtained from unsaved dirty settings. + * @param {Object} [args] - Additional options for the save request. + * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft. + * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server. + * @param {string} [args.title] - Title to update in the changeset. Optional. + * @param {string} [args.date] - Date to update in the changeset. Optional. + * @return {jQuery.Promise} Promise resolving with the response data. + */ + api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) { + var deferred, request, submittedChanges = {}, data, submittedArgs; + deferred = new $.Deferred(); + + // Prevent attempting changeset update while request is being made. + if ( 0 !== api.state( 'processing' ).get() ) { + deferred.reject( 'already_processing' ); + return deferred.promise(); + } + + submittedArgs = _.extend( { + title: null, + date: null, + autosave: false, + force: false + }, args ); + + if ( changes ) { + _.extend( submittedChanges, changes ); + } + + // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes. + _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) { + if ( ! changes || null !== changes[ settingId ] ) { + submittedChanges[ settingId ] = _.extend( + {}, + submittedChanges[ settingId ] || {}, + { value: dirtyValue } + ); + } + } ); + + // Allow plugins to attach additional params to the settings. + api.trigger( 'changeset-save', submittedChanges, submittedArgs ); + + // Short-circuit when there are no pending changes. + if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) { + deferred.resolve( {} ); + return deferred.promise(); + } + + // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used. + // Status is also disallowed for revisions regardless. + if ( submittedArgs.status ) { + return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise(); + } + + // Dates not beung allowed for revisions are is a technical limitation of post revisions. + if ( submittedArgs.date && submittedArgs.autosave ) { + return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise(); + } + + // Make sure that publishing a changeset waits for all changeset update requests to complete. + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); + deferred.always( function() { + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); + } ); + + // Ensure that if any plugins add data to save requests by extending query() that they get included here. + data = api.previewer.query( { excludeCustomizedSaved: true } ); + delete data.customized; // Being sent in customize_changeset_data instead. + _.extend( data, { + nonce: api.settings.nonce.save, + customize_theme: api.settings.theme.stylesheet, + customize_changeset_data: JSON.stringify( submittedChanges ) + } ); + if ( null !== submittedArgs.title ) { + data.customize_changeset_title = submittedArgs.title; + } + if ( null !== submittedArgs.date ) { + data.customize_changeset_date = submittedArgs.date; + } + if ( false !== submittedArgs.autosave ) { + data.customize_changeset_autosave = 'true'; + } + + // Allow plugins to modify the params included with the save request. + api.trigger( 'save-request-params', data ); + + request = wp.ajax.post( 'customize_save', data ); + + request.done( function requestChangesetUpdateDone( data ) { + var savedChangesetValues = {}; + + // Ensure that all settings updated subsequently will be included in the next changeset update request. + api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision ); + + api.state( 'changesetStatus' ).set( data.changeset_status ); + + if ( data.changeset_date ) { + api.state( 'changesetDate' ).set( data.changeset_date ); + } + + deferred.resolve( data ); + api.trigger( 'changeset-saved', data ); + + if ( data.setting_validities ) { + _.each( data.setting_validities, function( validity, settingId ) { + if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) { + savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value; + } + } ); + } + + api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) ); + } ); + request.fail( function requestChangesetUpdateFail( data ) { + deferred.reject( data ); + api.trigger( 'changeset-error', data ); + } ); + request.always( function( data ) { + if ( data.setting_validities ) { + api._handleSettingValidities( { + settingValidities: data.setting_validities + } ); + } + } ); + + return deferred.promise(); + }; + + /** + * Watch all changes to Value properties, and bubble changes to parent Values instance + * + * @alias wp.customize.utils.bubbleChildValueChanges + * + * @since 4.1.0 + * + * @param {wp.customize.Class} instance + * @param {Array} properties The names of the Value instances to watch. + */ + api.utils.bubbleChildValueChanges = function ( instance, properties ) { + $.each( properties, function ( i, key ) { + instance[ key ].bind( function ( to, from ) { + if ( instance.parent && to !== from ) { + instance.parent.trigger( 'change', instance ); + } + } ); + } ); + }; + + /** + * Expand a panel, section, or control and focus on the first focusable element. + * + * @alias wp.customize~focus + * + * @since 4.1.0 + * + * @param {Object} [params] + * @param {Function} [params.completeCallback] + */ + focus = function ( params ) { + var construct, completeCallback, focus, focusElement, sections; + construct = this; + params = params || {}; + focus = function () { + // If a child section is currently expanded, collapse it. + if ( construct.extended( api.Panel ) ) { + sections = construct.sections(); + if ( 1 < sections.length ) { + sections.forEach( function ( section ) { + if ( section.expanded() ) { + section.collapse(); + } + } ); + } + } + + var focusContainer; + if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) { + focusContainer = construct.contentContainer; + } else { + focusContainer = construct.container; + } + + focusElement = focusContainer.find( '.control-focus:first' ); + if ( 0 === focusElement.length ) { + // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583 + focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first(); + } + focusElement.focus(); + }; + if ( params.completeCallback ) { + completeCallback = params.completeCallback; + params.completeCallback = function () { + focus(); + completeCallback(); + }; + } else { + params.completeCallback = focus; + } + + api.state( 'paneVisible' ).set( true ); + if ( construct.expand ) { + construct.expand( params ); + } else { + params.completeCallback(); + } + }; + + /** + * Stable sort for Panels, Sections, and Controls. + * + * If a.priority() === b.priority(), then sort by their respective params.instanceNumber. + * + * @alias wp.customize.utils.prioritySort + * + * @since 4.1.0 + * + * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a + * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b + * @return {number} + */ + api.utils.prioritySort = function ( a, b ) { + if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) { + return a.params.instanceNumber - b.params.instanceNumber; + } else { + return a.priority() - b.priority(); + } + }; + + /** + * Return whether the supplied Event object is for a keydown event but not the Enter key. + * + * @alias wp.customize.utils.isKeydownButNotEnterEvent + * + * @since 4.1.0 + * + * @param {jQuery.Event} event + * @return {boolean} + */ + api.utils.isKeydownButNotEnterEvent = function ( event ) { + return ( 'keydown' === event.type && 13 !== event.which ); + }; + + /** + * Return whether the two lists of elements are the same and are in the same order. + * + * @alias wp.customize.utils.areElementListsEqual + * + * @since 4.1.0 + * + * @param {Array|jQuery} listA + * @param {Array|jQuery} listB + * @return {boolean} + */ + api.utils.areElementListsEqual = function ( listA, listB ) { + var equal = ( + listA.length === listB.length && // If lists are different lengths, then naturally they are not equal. + -1 === _.indexOf( _.map( // Are there any false values in the list returned by map? + _.zip( listA, listB ), // Pair up each element between the two lists. + function ( pair ) { + return $( pair[0] ).is( pair[1] ); // Compare to see if each pair is equal. + } + ), false ) // Check for presence of false in map's return value. + ); + return equal; + }; + + /** + * Highlight the existence of a button. + * + * This function reminds the user of a button represented by the specified + * UI element, after an optional delay. If the user focuses the element + * before the delay passes, the reminder is canceled. + * + * @alias wp.customize.utils.highlightButton + * + * @since 4.9.0 + * + * @param {jQuery} button - The element to highlight. + * @param {Object} [options] - Options. + * @param {number} [options.delay=0] - Delay in milliseconds. + * @param {jQuery} [options.focusTarget] - A target for user focus that defaults to the highlighted element. + * If the user focuses the target before the delay passes, the reminder + * is canceled. This option exists to accommodate compound buttons + * containing auxiliary UI, such as the Publish button augmented with a + * Settings button. + * @return {Function} An idempotent function that cancels the reminder. + */ + api.utils.highlightButton = function highlightButton( button, options ) { + var animationClass = 'button-see-me', + canceled = false, + params; + + params = _.extend( + { + delay: 0, + focusTarget: button + }, + options + ); + + function cancelReminder() { + canceled = true; + } + + params.focusTarget.on( 'focusin', cancelReminder ); + setTimeout( function() { + params.focusTarget.off( 'focusin', cancelReminder ); + + if ( ! canceled ) { + button.addClass( animationClass ); + button.one( 'animationend', function() { + /* + * Remove animation class to avoid situations in Customizer where + * DOM nodes are moved (re-inserted) and the animation repeats. + */ + button.removeClass( animationClass ); + } ); + } + }, params.delay ); + + return cancelReminder; + }; + + /** + * Get current timestamp adjusted for server clock time. + * + * Same functionality as the `current_time( 'mysql', false )` function in PHP. + * + * @alias wp.customize.utils.getCurrentTimestamp + * + * @since 4.9.0 + * + * @return {number} Current timestamp. + */ + api.utils.getCurrentTimestamp = function getCurrentTimestamp() { + var currentDate, currentClientTimestamp, timestampDifferential; + currentClientTimestamp = _.now(); + currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) ); + timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp; + timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp; + currentDate.setTime( currentDate.getTime() + timestampDifferential ); + return currentDate.getTime(); + }; + + /** + * Get remaining time of when the date is set. + * + * @alias wp.customize.utils.getRemainingTime + * + * @since 4.9.0 + * + * @param {string|number|Date} datetime - Date time or timestamp of the future date. + * @return {number} remainingTime - Remaining time in milliseconds. + */ + api.utils.getRemainingTime = function getRemainingTime( datetime ) { + var millisecondsDivider = 1000, remainingTime, timestamp; + if ( datetime instanceof Date ) { + timestamp = datetime.getTime(); + } else if ( 'string' === typeof datetime ) { + timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime(); + } else { + timestamp = datetime; + } + + remainingTime = timestamp - api.utils.getCurrentTimestamp(); + remainingTime = Math.ceil( remainingTime / millisecondsDivider ); + return remainingTime; + }; + + /** + * Return browser supported `transitionend` event name. + * + * @since 4.7.0 + * + * @ignore + * + * @return {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported. + */ + normalizedTransitionendEventName = (function () { + var el, transitions, prop; + el = document.createElement( 'div' ); + transitions = { + 'transition' : 'transitionend', + 'OTransition' : 'oTransitionEnd', + 'MozTransition' : 'transitionend', + 'WebkitTransition': 'webkitTransitionEnd' + }; + prop = _.find( _.keys( transitions ), function( prop ) { + return ! _.isUndefined( el.style[ prop ] ); + } ); + if ( prop ) { + return transitions[ prop ]; + } else { + return null; + } + })(); + + Container = api.Class.extend(/** @lends wp.customize~Container.prototype */{ + defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, + defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop }, + containerType: 'container', + defaults: { + title: '', + description: '', + priority: 100, + type: 'default', + content: null, + active: true, + instanceNumber: null + }, + + /** + * Base class for Panel and Section. + * + * @constructs wp.customize~Container + * @augments wp.customize.Class + * + * @since 4.1.0 + * + * @borrows wp.customize~focus as focus + * + * @param {string} id - The ID for the container. + * @param {Object} options - Object containing one property: params. + * @param {string} options.title - Title shown when panel is collapsed and expanded. + * @param {string} [options.description] - Description shown at the top of the panel. + * @param {number} [options.priority=100] - The sort priority for the panel. + * @param {string} [options.templateId] - Template selector for container. + * @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor. + * @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used. + * @param {boolean} [options.active=true] - Whether the panel is active or not. + * @param {Object} [options.params] - Deprecated wrapper for the above properties. + */ + initialize: function ( id, options ) { + var container = this; + container.id = id; + + if ( ! Container.instanceCounter ) { + Container.instanceCounter = 0; + } + Container.instanceCounter++; + + $.extend( container, { + params: _.defaults( + options.params || options, // Passing the params is deprecated. + container.defaults + ) + } ); + if ( ! container.params.instanceNumber ) { + container.params.instanceNumber = Container.instanceCounter; + } + container.notifications = new api.Notifications(); + container.templateSelector = container.params.templateId || 'customize-' + container.containerType + '-' + container.params.type; + container.container = $( container.params.content ); + if ( 0 === container.container.length ) { + container.container = $( container.getContainer() ); + } + container.headContainer = container.container; + container.contentContainer = container.getContent(); + container.container = container.container.add( container.contentContainer ); + + container.deferred = { + embedded: new $.Deferred() + }; + container.priority = new api.Value(); + container.active = new api.Value(); + container.activeArgumentsQueue = []; + container.expanded = new api.Value(); + container.expandedArgumentsQueue = []; + + container.active.bind( function ( active ) { + var args = container.activeArgumentsQueue.shift(); + args = $.extend( {}, container.defaultActiveArguments, args ); + active = ( active && container.isContextuallyActive() ); + container.onChangeActive( active, args ); + }); + container.expanded.bind( function ( expanded ) { + var args = container.expandedArgumentsQueue.shift(); + args = $.extend( {}, container.defaultExpandedArguments, args ); + container.onChangeExpanded( expanded, args ); + }); + + container.deferred.embedded.done( function () { + container.setupNotifications(); + container.attachEvents(); + }); + + api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] ); + + container.priority.set( container.params.priority ); + container.active.set( container.params.active ); + container.expanded.set( false ); + }, + + /** + * Get the element that will contain the notifications. + * + * @since 4.9.0 + * @return {jQuery} Notification container element. + */ + getNotificationsContainerElement: function() { + var container = this; + return container.contentContainer.find( '.customize-control-notifications-container:first' ); + }, + + /** + * Set up notifications. + * + * @since 4.9.0 + * @return {void} + */ + setupNotifications: function() { + var container = this, renderNotifications; + container.notifications.container = container.getNotificationsContainerElement(); + + // Render notifications when they change and when the construct is expanded. + renderNotifications = function() { + if ( container.expanded.get() ) { + container.notifications.render(); + } + }; + container.expanded.bind( renderNotifications ); + renderNotifications(); + container.notifications.bind( 'change', _.debounce( renderNotifications ) ); + }, + + /** + * @since 4.1.0 + * + * @abstract + */ + ready: function() {}, + + /** + * Get the child models associated with this parent, sorting them by their priority Value. + * + * @since 4.1.0 + * + * @param {string} parentType + * @param {string} childType + * @return {Array} + */ + _children: function ( parentType, childType ) { + var parent = this, + children = []; + api[ childType ].each( function ( child ) { + if ( child[ parentType ].get() === parent.id ) { + children.push( child ); + } + } ); + children.sort( api.utils.prioritySort ); + return children; + }, + + /** + * To override by subclass, to return whether the container has active children. + * + * @since 4.1.0 + * + * @abstract + */ + isContextuallyActive: function () { + throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' ); + }, + + /** + * Active state change handler. + * + * Shows the container if it is active, hides it if not. + * + * To override by subclass, update the container's UI to reflect the provided active state. + * + * @since 4.1.0 + * + * @param {boolean} active - The active state to transiution to. + * @param {Object} [args] - Args. + * @param {Object} [args.duration] - The duration for the slideUp/slideDown animation. + * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early. + * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed. + */ + onChangeActive: function( active, args ) { + var construct = this, + headContainer = construct.headContainer, + duration, expandedOtherPanel; + + if ( args.unchanged ) { + if ( args.completeCallback ) { + args.completeCallback(); + } + return; + } + + duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 ); + + if ( construct.extended( api.Panel ) ) { + // If this is a panel is not currently expanded but another panel is expanded, do not animate. + api.panel.each(function ( panel ) { + if ( panel !== construct && panel.expanded() ) { + expandedOtherPanel = panel; + duration = 0; + } + }); + + // Collapse any expanded sections inside of this panel first before deactivating. + if ( ! active ) { + _.each( construct.sections(), function( section ) { + section.collapse( { duration: 0 } ); + } ); + } + } + + if ( ! $.contains( document, headContainer.get( 0 ) ) ) { + // If the element is not in the DOM, then jQuery.fn.slideUp() does nothing. + // In this case, a hard toggle is required instead. + headContainer.toggle( active ); + if ( args.completeCallback ) { + args.completeCallback(); + } + } else if ( active ) { + headContainer.slideDown( duration, args.completeCallback ); + } else { + if ( construct.expanded() ) { + construct.collapse({ + duration: duration, + completeCallback: function() { + headContainer.slideUp( duration, args.completeCallback ); + } + }); + } else { + headContainer.slideUp( duration, args.completeCallback ); + } + } + }, + + /** + * @since 4.1.0 + * + * @param {boolean} active + * @param {Object} [params] + * @return {boolean} False if state already applied. + */ + _toggleActive: function ( active, params ) { + var self = this; + params = params || {}; + if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) { + params.unchanged = true; + self.onChangeActive( self.active.get(), params ); + return false; + } else { + params.unchanged = false; + this.activeArgumentsQueue.push( params ); + this.active.set( active ); + return true; + } + }, + + /** + * @param {Object} [params] + * @return {boolean} False if already active. + */ + activate: function ( params ) { + return this._toggleActive( true, params ); + }, + + /** + * @param {Object} [params] + * @return {boolean} False if already inactive. + */ + deactivate: function ( params ) { + return this._toggleActive( false, params ); + }, + + /** + * To override by subclass, update the container's UI to reflect the provided active state. + * @abstract + */ + onChangeExpanded: function () { + throw new Error( 'Must override with subclass.' ); + }, + + /** + * Handle the toggle logic for expand/collapse. + * + * @param {boolean} expanded - The new state to apply. + * @param {Object} [params] - Object containing options for expand/collapse. + * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete. + * @return {boolean} False if state already applied or active state is false. + */ + _toggleExpanded: function( expanded, params ) { + var instance = this, previousCompleteCallback; + params = params || {}; + previousCompleteCallback = params.completeCallback; + + // Short-circuit expand() if the instance is not active. + if ( expanded && ! instance.active() ) { + return false; + } + + api.state( 'paneVisible' ).set( true ); + params.completeCallback = function() { + if ( previousCompleteCallback ) { + previousCompleteCallback.apply( instance, arguments ); + } + if ( expanded ) { + instance.container.trigger( 'expanded' ); + } else { + instance.container.trigger( 'collapsed' ); + } + }; + if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) { + params.unchanged = true; + instance.onChangeExpanded( instance.expanded.get(), params ); + return false; + } else { + params.unchanged = false; + instance.expandedArgumentsQueue.push( params ); + instance.expanded.set( expanded ); + return true; + } + }, + + /** + * @param {Object} [params] + * @return {boolean} False if already expanded or if inactive. + */ + expand: function ( params ) { + return this._toggleExpanded( true, params ); + }, + + /** + * @param {Object} [params] + * @return {boolean} False if already collapsed. + */ + collapse: function ( params ) { + return this._toggleExpanded( false, params ); + }, + + /** + * Animate container state change if transitions are supported by the browser. + * + * @since 4.7.0 + * @private + * + * @param {function} completeCallback Function to be called after transition is completed. + * @return {void} + */ + _animateChangeExpanded: function( completeCallback ) { + // Return if CSS transitions are not supported or if reduced motion is enabled. + if ( ! normalizedTransitionendEventName || isReducedMotion ) { + // Schedule the callback until the next tick to prevent focus loss. + _.defer( function () { + if ( completeCallback ) { + completeCallback(); + } + } ); + return; + } + + var construct = this, + content = construct.contentContainer, + overlay = content.closest( '.wp-full-overlay' ), + elements, transitionEndCallback, transitionParentPane; + + // Determine set of elements that are affected by the animation. + elements = overlay.add( content ); + + if ( ! construct.panel || '' === construct.panel() ) { + transitionParentPane = true; + } else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) { + transitionParentPane = true; + } else { + transitionParentPane = false; + } + if ( transitionParentPane ) { + elements = elements.add( '#customize-info, .customize-pane-parent' ); + } + + // Handle `transitionEnd` event. + transitionEndCallback = function( e ) { + if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) { + return; + } + content.off( normalizedTransitionendEventName, transitionEndCallback ); + elements.removeClass( 'busy' ); + if ( completeCallback ) { + completeCallback(); + } + }; + content.on( normalizedTransitionendEventName, transitionEndCallback ); + elements.addClass( 'busy' ); + + // Prevent screen flicker when pane has been scrolled before expanding. + _.defer( function() { + var container = content.closest( '.wp-full-overlay-sidebar-content' ), + currentScrollTop = container.scrollTop(), + previousScrollTop = content.data( 'previous-scrollTop' ) || 0, + expanded = construct.expanded(); + + if ( expanded && 0 < currentScrollTop ) { + content.css( 'top', currentScrollTop + 'px' ); + content.data( 'previous-scrollTop', currentScrollTop ); + } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) { + content.css( 'top', previousScrollTop - currentScrollTop + 'px' ); + container.scrollTop( previousScrollTop ); + } + } ); + }, + + /* + * is documented using @borrows in the constructor. + */ + focus: focus, + + /** + * Return the container html, generated from its JS template, if it exists. + * + * @since 4.3.0 + */ + getContainer: function () { + var template, + container = this; + + if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) { + template = wp.template( container.templateSelector ); + } else { + template = wp.template( 'customize-' + container.containerType + '-default' ); + } + if ( template && container.container ) { + return template( _.extend( + { id: container.id }, + container.params + ) ).toString().trim(); + } + + return '<li></li>'; + }, + + /** + * Find content element which is displayed when the section is expanded. + * + * After a construct is initialized, the return value will be available via the `contentContainer` property. + * By default the element will be related it to the parent container with `aria-owns` and detached. + * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should + * just return the content element without needing to add the `aria-owns` element or detach it from + * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded` + * method to handle animating the panel/section into and out of view. + * + * @since 4.7.0 + * @access public + * + * @return {jQuery} Detached content element. + */ + getContent: function() { + var construct = this, + container = construct.container, + content = container.find( '.accordion-section-content, .control-panel-content' ).first(), + contentId = 'sub-' + container.attr( 'id' ), + ownedElements = contentId, + alreadyOwnedElements = container.attr( 'aria-owns' ); + + if ( alreadyOwnedElements ) { + ownedElements = ownedElements + ' ' + alreadyOwnedElements; + } + container.attr( 'aria-owns', ownedElements ); + + return content.detach().attr( { + 'id': contentId, + 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' ) + } ); + } + }); + + api.Section = Container.extend(/** @lends wp.customize.Section.prototype */{ + containerType: 'section', + containerParent: '#customize-theme-controls', + containerPaneParent: '.customize-pane-parent', + defaults: { + title: '', + description: '', + priority: 100, + type: 'default', + content: null, + active: true, + instanceNumber: null, + panel: null, + customizeAction: '' + }, + + /** + * @constructs wp.customize.Section + * @augments wp.customize~Container + * + * @since 4.1.0 + * + * @param {string} id - The ID for the section. + * @param {Object} options - Options. + * @param {string} options.title - Title shown when section is collapsed and expanded. + * @param {string} [options.description] - Description shown at the top of the section. + * @param {number} [options.priority=100] - The sort priority for the section. + * @param {string} [options.type=default] - The type of the section. See wp.customize.sectionConstructor. + * @param {string} [options.content] - The markup to be used for the section container. If empty, a JS template is used. + * @param {boolean} [options.active=true] - Whether the section is active or not. + * @param {string} options.panel - The ID for the panel this section is associated with. + * @param {string} [options.customizeAction] - Additional context information shown before the section title when expanded. + * @param {Object} [options.params] - Deprecated wrapper for the above properties. + */ + initialize: function ( id, options ) { + var section = this, params; + params = options.params || options; + + // Look up the type if one was not supplied. + if ( ! params.type ) { + _.find( api.sectionConstructor, function( Constructor, type ) { + if ( Constructor === section.constructor ) { + params.type = type; + return true; + } + return false; + } ); + } + + Container.prototype.initialize.call( section, id, params ); + + section.id = id; + section.panel = new api.Value(); + section.panel.bind( function ( id ) { + $( section.headContainer ).toggleClass( 'control-subsection', !! id ); + }); + section.panel.set( section.params.panel || '' ); + api.utils.bubbleChildValueChanges( section, [ 'panel' ] ); + + section.embed(); + section.deferred.embedded.done( function () { + section.ready(); + }); + }, + + /** + * Embed the container in the DOM when any parent panel is ready. + * + * @since 4.1.0 + */ + embed: function () { + var inject, + section = this; + + section.containerParent = api.ensure( section.containerParent ); + + // Watch for changes to the panel state. + inject = function ( panelId ) { + var parentContainer; + if ( panelId ) { + // The panel has been supplied, so wait until the panel object is registered. + api.panel( panelId, function ( panel ) { + // The panel has been registered, wait for it to become ready/initialized. + panel.deferred.embedded.done( function () { + parentContainer = panel.contentContainer; + if ( ! section.headContainer.parent().is( parentContainer ) ) { + parentContainer.append( section.headContainer ); + } + if ( ! section.contentContainer.parent().is( section.headContainer ) ) { + section.containerParent.append( section.contentContainer ); + } + section.deferred.embedded.resolve(); + }); + } ); + } else { + // There is no panel, so embed the section in the root of the customizer. + parentContainer = api.ensure( section.containerPaneParent ); + if ( ! section.headContainer.parent().is( parentContainer ) ) { + parentContainer.append( section.headContainer ); + } + if ( ! section.contentContainer.parent().is( section.headContainer ) ) { + section.containerParent.append( section.contentContainer ); + } + section.deferred.embedded.resolve(); + } + }; + section.panel.bind( inject ); + inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one. + }, + + /** + * Add behaviors for the accordion section. + * + * @since 4.1.0 + */ + attachEvents: function () { + var meta, content, section = this; + + if ( section.container.hasClass( 'cannot-expand' ) ) { + return; + } + + // Expand/Collapse accordion sections on click. + section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above. + + if ( section.expanded() ) { + section.collapse(); + } else { + section.expand(); + } + }); + + // This is very similar to what is found for api.Panel.attachEvents(). + section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() { + + meta = section.container.find( '.section-meta' ); + if ( meta.hasClass( 'cannot-expand' ) ) { + return; + } + content = meta.find( '.customize-section-description:first' ); + content.toggleClass( 'open' ); + content.slideToggle( section.defaultExpandedArguments.duration, function() { + content.trigger( 'toggled' ); + } ); + $( this ).attr( 'aria-expanded', function( i, attr ) { + return 'true' === attr ? 'false' : 'true'; + }); + }); + }, + + /** + * Return whether this section has any active controls. + * + * @since 4.1.0 + * + * @return {boolean} + */ + isContextuallyActive: function () { + var section = this, + controls = section.controls(), + activeCount = 0; + _( controls ).each( function ( control ) { + if ( control.active() ) { + activeCount += 1; + } + } ); + return ( activeCount !== 0 ); + }, + + /** + * Get the controls that are associated with this section, sorted by their priority Value. + * + * @since 4.1.0 + * + * @return {Array} + */ + controls: function () { + return this._children( 'section', 'control' ); + }, + + /** + * Update UI to reflect expanded state. + * + * @since 4.1.0 + * + * @param {boolean} expanded + * @param {Object} args + */ + onChangeExpanded: function ( expanded, args ) { + var section = this, + container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ), + content = section.contentContainer, + overlay = section.headContainer.closest( '.wp-full-overlay' ), + backBtn = content.find( '.customize-section-back' ), + sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(), + expand, panel; + + if ( expanded && ! content.hasClass( 'open' ) ) { + + if ( args.unchanged ) { + expand = args.completeCallback; + } else { + expand = function() { + section._animateChangeExpanded( function() { + sectionTitle.attr( 'tabindex', '-1' ); + backBtn.attr( 'tabindex', '0' ); + + backBtn.trigger( 'focus' ); + content.css( 'top', '' ); + container.scrollTop( 0 ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + + content.addClass( 'open' ); + overlay.addClass( 'section-open' ); + api.state( 'expandedSection' ).set( section ); + }.bind( this ); + } + + if ( ! args.allowMultiple ) { + api.section.each( function ( otherSection ) { + if ( otherSection !== section ) { + otherSection.collapse( { duration: args.duration } ); + } + }); + } + + if ( section.panel() ) { + api.panel( section.panel() ).expand({ + duration: args.duration, + completeCallback: expand + }); + } else { + if ( ! args.allowMultiple ) { + api.panel.each( function( panel ) { + panel.collapse(); + }); + } + expand(); + } + + } else if ( ! expanded && content.hasClass( 'open' ) ) { + if ( section.panel() ) { + panel = api.panel( section.panel() ); + if ( panel.contentContainer.hasClass( 'skip-transition' ) ) { + panel.collapse(); + } + } + section._animateChangeExpanded( function() { + backBtn.attr( 'tabindex', '-1' ); + sectionTitle.attr( 'tabindex', '0' ); + + sectionTitle.trigger( 'focus' ); + content.css( 'top', '' ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + + content.removeClass( 'open' ); + overlay.removeClass( 'section-open' ); + if ( section === api.state( 'expandedSection' ).get() ) { + api.state( 'expandedSection' ).set( false ); + } + + } else { + if ( args.completeCallback ) { + args.completeCallback(); + } + } + } + }); + + api.ThemesSection = api.Section.extend(/** @lends wp.customize.ThemesSection.prototype */{ + currentTheme: '', + overlay: '', + template: '', + screenshotQueue: null, + $window: null, + $body: null, + loaded: 0, + loading: false, + fullyLoaded: false, + term: '', + tags: '', + nextTerm: '', + nextTags: '', + filtersHeight: 0, + headerContainer: null, + updateCountDebounced: null, + + /** + * wp.customize.ThemesSection + * + * Custom section for themes that loads themes by category, and also + * handles the theme-details view rendering and navigation. + * + * @constructs wp.customize.ThemesSection + * @augments wp.customize.Section + * + * @since 4.9.0 + * + * @param {string} id - ID. + * @param {Object} options - Options. + * @return {void} + */ + initialize: function( id, options ) { + var section = this; + section.headerContainer = $(); + section.$window = $( window ); + section.$body = $( document.body ); + api.Section.prototype.initialize.call( section, id, options ); + section.updateCountDebounced = _.debounce( section.updateCount, 500 ); + }, + + /** + * Embed the section in the DOM when the themes panel is ready. + * + * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel. + * + * @since 4.9.0 + */ + embed: function() { + var inject, + section = this; + + // Watch for changes to the panel state. + inject = function( panelId ) { + var parentContainer; + api.panel( panelId, function( panel ) { + + // The panel has been registered, wait for it to become ready/initialized. + panel.deferred.embedded.done( function() { + parentContainer = panel.contentContainer; + if ( ! section.headContainer.parent().is( parentContainer ) ) { + parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer ); + } + if ( ! section.contentContainer.parent().is( section.headContainer ) ) { + section.containerParent.append( section.contentContainer ); + } + section.deferred.embedded.resolve(); + }); + } ); + }; + section.panel.bind( inject ); + inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one. + }, + + /** + * Set up. + * + * @since 4.2.0 + * + * @return {void} + */ + ready: function() { + var section = this; + section.overlay = section.container.find( '.theme-overlay' ); + section.template = wp.template( 'customize-themes-details-view' ); + + // Bind global keyboard events. + section.container.on( 'keydown', function( event ) { + if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) { + return; + } + + // Pressing the right arrow key fires a theme:next event. + if ( 39 === event.keyCode ) { + section.nextTheme(); + } + + // Pressing the left arrow key fires a theme:previous event. + if ( 37 === event.keyCode ) { + section.previousTheme(); + } + + // Pressing the escape key fires a theme:collapse event. + if ( 27 === event.keyCode ) { + if ( section.$body.hasClass( 'modal-open' ) ) { + + // Escape from the details modal. + section.closeDetails(); + } else { + + // Escape from the inifinite scroll list. + section.headerContainer.find( '.customize-themes-section-title' ).focus(); + } + event.stopPropagation(); // Prevent section from being collapsed. + } + }); + + section.renderScreenshots = _.throttle( section.renderScreenshots, 100 ); + + _.bindAll( section, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' ); + }, + + /** + * Override Section.isContextuallyActive method. + * + * Ignore the active states' of the contained theme controls, and just + * use the section's own active state instead. This prevents empty search + * results for theme sections from causing the section to become inactive. + * + * @since 4.2.0 + * + * @return {boolean} + */ + isContextuallyActive: function () { + return this.active(); + }, + + /** + * Attach events. + * + * @since 4.2.0 + * + * @return {void} + */ + attachEvents: function () { + var section = this, debounced; + + // Expand/Collapse accordion sections on click. + section.container.find( '.customize-section-back' ).on( 'click keydown', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above. + section.collapse(); + }); + + section.headerContainer = $( '#accordion-section-' + section.id ); + + // Expand section/panel. Only collapse when opening another section. + section.headerContainer.on( 'click', '.customize-themes-section-title', function() { + + // Toggle accordion filters under section headers. + if ( section.headerContainer.find( '.filter-details' ).length ) { + section.headerContainer.find( '.customize-themes-section-title' ) + .toggleClass( 'details-open' ) + .attr( 'aria-expanded', function( i, attr ) { + return 'true' === attr ? 'false' : 'true'; + }); + section.headerContainer.find( '.filter-details' ).slideToggle( 180 ); + } + + // Open the section. + if ( ! section.expanded() ) { + section.expand(); + } + }); + + // Preview installed themes. + section.container.on( 'click', '.theme-actions .preview-theme', function() { + api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) ); + }); + + // Theme navigation in details view. + section.container.on( 'click', '.left', function() { + section.previousTheme(); + }); + + section.container.on( 'click', '.right', function() { + section.nextTheme(); + }); + + section.container.on( 'click', '.theme-backdrop, .close', function() { + section.closeDetails(); + }); + + if ( 'local' === section.params.filter_type ) { + + // Filter-search all theme objects loaded in the section. + section.container.on( 'input', '.wp-filter-search-themes', function( event ) { + section.filterSearch( event.currentTarget.value ); + }); + + } else if ( 'remote' === section.params.filter_type ) { + + // Event listeners for remote queries with user-entered terms. + // Search terms. + debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search. + section.contentContainer.on( 'input', '.wp-filter-search', function() { + if ( ! api.panel( 'themes' ).expanded() ) { + return; + } + debounced( section ); + if ( ! section.expanded() ) { + section.expand(); + } + }); + + // Feature filters. + section.contentContainer.on( 'click', '.filter-group input', function() { + section.filtersChecked(); + section.checkTerm( section ); + }); + } + + // Toggle feature filters. + section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) { + var $themeContainer = $( '.customize-themes-full-container' ), + $filterToggle = $( e.currentTarget ); + section.filtersHeight = $filterToggle.parent().next( '.filter-drawer' ).height(); + + if ( 0 < $themeContainer.scrollTop() ) { + $themeContainer.animate( { scrollTop: 0 }, 400 ); + + if ( $filterToggle.hasClass( 'open' ) ) { + return; + } + } + + $filterToggle + .toggleClass( 'open' ) + .attr( 'aria-expanded', function( i, attr ) { + return 'true' === attr ? 'false' : 'true'; + }) + .parent().next( '.filter-drawer' ).slideToggle( 180, 'linear' ); + + if ( $filterToggle.hasClass( 'open' ) ) { + var marginOffset = 1018 < window.innerWidth ? 50 : 76; + + section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + marginOffset ); + } else { + section.contentContainer.find( '.themes' ).css( 'margin-top', 0 ); + } + }); + + // Setup section cross-linking. + section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() { + api.section( 'wporg_themes' ).focus(); + }); + + function updateSelectedState() { + var el = section.headerContainer.find( '.customize-themes-section-title' ); + el.toggleClass( 'selected', section.expanded() ); + el.attr( 'aria-expanded', section.expanded() ? 'true' : 'false' ); + if ( ! section.expanded() ) { + el.removeClass( 'details-open' ); + } + } + section.expanded.bind( updateSelectedState ); + updateSelectedState(); + + // Move section controls to the themes area. + api.bind( 'ready', function () { + section.contentContainer = section.container.find( '.customize-themes-section' ); + section.contentContainer.appendTo( $( '.customize-themes-full-container' ) ); + section.container.add( section.headerContainer ); + }); + }, + + /** + * Update UI to reflect expanded state + * + * @since 4.2.0 + * + * @param {boolean} expanded + * @param {Object} args + * @param {boolean} args.unchanged + * @param {Function} args.completeCallback + * @return {void} + */ + onChangeExpanded: function ( expanded, args ) { + + // Note: there is a second argument 'args' passed. + var section = this, + container = section.contentContainer.closest( '.customize-themes-full-container' ); + + // Immediately call the complete callback if there were no changes. + if ( args.unchanged ) { + if ( args.completeCallback ) { + args.completeCallback(); + } + return; + } + + function expand() { + + // Try to load controls if none are loaded yet. + if ( 0 === section.loaded ) { + section.loadThemes(); + } + + // Collapse any sibling sections/panels. + api.section.each( function ( otherSection ) { + var searchTerm; + + if ( otherSection !== section ) { + + // Try to sync the current search term to the new section. + if ( 'themes' === otherSection.params.type ) { + searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val(); + section.contentContainer.find( '.wp-filter-search' ).val( searchTerm ); + + // Directly initialize an empty remote search to avoid a race condition. + if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) { + section.term = ''; + section.initializeNewQuery( section.term, section.tags ); + } else { + if ( 'remote' === section.params.filter_type ) { + section.checkTerm( section ); + } else if ( 'local' === section.params.filter_type ) { + section.filterSearch( searchTerm ); + } + } + otherSection.collapse( { duration: args.duration } ); + } + } + }); + + section.contentContainer.addClass( 'current-section' ); + container.scrollTop(); + + container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) ); + container.on( 'scroll', _.throttle( section.loadMore, 300 ) ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + section.updateCount(); // Show this section's count. + } + + if ( expanded ) { + if ( section.panel() && api.panel.has( section.panel() ) ) { + api.panel( section.panel() ).expand({ + duration: args.duration, + completeCallback: expand + }); + } else { + expand(); + } + } else { + section.contentContainer.removeClass( 'current-section' ); + + // Always hide, even if they don't exist or are already hidden. + section.headerContainer.find( '.filter-details' ).slideUp( 180 ); + + container.off( 'scroll' ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } + }, + + /** + * Return the section's content element without detaching from the parent. + * + * @since 4.9.0 + * + * @return {jQuery} + */ + getContent: function() { + return this.container.find( '.control-section-content' ); + }, + + /** + * Load theme data via Ajax and add themes to the section as controls. + * + * @since 4.9.0 + * + * @return {void} + */ + loadThemes: function() { + var section = this, params, page, request; + + if ( section.loading ) { + return; // We're already loading a batch of themes. + } + + // Parameters for every API query. Additional params are set in PHP. + page = Math.ceil( section.loaded / 100 ) + 1; + params = { + 'nonce': api.settings.nonce.switch_themes, + 'wp_customize': 'on', + 'theme_action': section.params.action, + 'customized_theme': api.settings.theme.stylesheet, + 'page': page + }; + + // Add fields for remote filtering. + if ( 'remote' === section.params.filter_type ) { + params.search = section.term; + params.tags = section.tags; + } + + // Load themes. + section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' ); + section.loading = true; + section.container.find( '.no-themes' ).hide(); + request = wp.ajax.post( 'customize_load_themes', params ); + request.done(function( data ) { + var themes = data.themes; + + // Stop and try again if the term changed while loading. + if ( '' !== section.nextTerm || '' !== section.nextTags ) { + if ( section.nextTerm ) { + section.term = section.nextTerm; + } + if ( section.nextTags ) { + section.tags = section.nextTags; + } + section.nextTerm = ''; + section.nextTags = ''; + section.loading = false; + section.loadThemes(); + return; + } + + if ( 0 !== themes.length ) { + + section.loadControls( themes, page ); + + if ( 1 === page ) { + + // Pre-load the first 3 theme screenshots. + _.each( section.controls().slice( 0, 3 ), function( control ) { + var img, src = control.params.theme.screenshot[0]; + if ( src ) { + img = new Image(); + img.src = src; + } + }); + if ( 'local' !== section.params.filter_type ) { + wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) ); + } + } + + _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible. + + if ( 'local' === section.params.filter_type || 100 > themes.length ) { + // If we have less than the requested 100 themes, it's the end of the list. + section.fullyLoaded = true; + } + } else { + if ( 0 === section.loaded ) { + section.container.find( '.no-themes' ).show(); + wp.a11y.speak( section.container.find( '.no-themes' ).text() ); + } else { + section.fullyLoaded = true; + } + } + if ( 'local' === section.params.filter_type ) { + section.updateCount(); // Count of visible theme controls. + } else { + section.updateCount( data.info.results ); // Total number of results including pages not yet loaded. + } + section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown. + + // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case. + section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' ); + section.loading = false; + }); + request.fail(function( data ) { + if ( 'undefined' === typeof data ) { + section.container.find( '.unexpected-error' ).show(); + wp.a11y.speak( section.container.find( '.unexpected-error' ).text() ); + } else if ( 'undefined' !== typeof console && console.error ) { + console.error( data ); + } + + // This cannot run on request.always, as section.loading may turn false before the new controls load in the success case. + section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' ); + section.loading = false; + }); + }, + + /** + * Loads controls into the section from data received from loadThemes(). + * + * @since 4.9.0 + * @param {Array} themes - Array of theme data to create controls with. + * @param {number} page - Page of results being loaded. + * @return {void} + */ + loadControls: function( themes, page ) { + var newThemeControls = [], + section = this; + + // Add controls for each theme. + _.each( themes, function( theme ) { + var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, { + type: 'theme', + section: section.params.id, + theme: theme, + priority: section.loaded + 1 + } ); + + api.control.add( themeControl ); + newThemeControls.push( themeControl ); + section.loaded = section.loaded + 1; + }); + + if ( 1 !== page ) { + Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue. + } + }, + + /** + * Determines whether more themes should be loaded, and loads them. + * + * @since 4.9.0 + * @return {void} + */ + loadMore: function() { + var section = this, container, bottom, threshold; + if ( ! section.fullyLoaded && ! section.loading ) { + container = section.container.closest( '.customize-themes-full-container' ); + + bottom = container.scrollTop() + container.height(); + // Use a fixed distance to the bottom of loaded results to avoid unnecessarily + // loading results sooner when using a percentage of scroll distance. + threshold = container.prop( 'scrollHeight' ) - 3000; + + if ( bottom > threshold ) { + section.loadThemes(); + } + } + }, + + /** + * Event handler for search input that filters visible controls. + * + * @since 4.9.0 + * + * @param {string} term - The raw search input value. + * @return {void} + */ + filterSearch: function( term ) { + var count = 0, + visible = false, + section = this, + noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes', + controls = section.controls(), + terms; + + if ( section.loading ) { + return; + } + + // Standardize search term format and split into an array of individual words. + terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' ); + + _.each( controls, function( control ) { + visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term. + if ( visible ) { + count = count + 1; + } + }); + + if ( 0 === count ) { + section.container.find( noFilter ).show(); + wp.a11y.speak( section.container.find( noFilter ).text() ); + } else { + section.container.find( noFilter ).hide(); + } + + section.renderScreenshots(); + api.reflowPaneContents(); + + // Update theme count. + section.updateCountDebounced( count ); + }, + + /** + * Event handler for search input that determines if the terms have changed and loads new controls as needed. + * + * @since 4.9.0 + * + * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer. + * @return {void} + */ + checkTerm: function( section ) { + var newTerm; + if ( 'remote' === section.params.filter_type ) { + newTerm = section.contentContainer.find( '.wp-filter-search' ).val(); + if ( section.term !== newTerm.trim() ) { + section.initializeNewQuery( newTerm, section.tags ); + } + } + }, + + /** + * Check for filters checked in the feature filter list and initialize a new query. + * + * @since 4.9.0 + * + * @return {void} + */ + filtersChecked: function() { + var section = this, + items = section.container.find( '.filter-group' ).find( ':checkbox' ), + tags = []; + + _.each( items.filter( ':checked' ), function( item ) { + tags.push( $( item ).prop( 'value' ) ); + }); + + // When no filters are checked, restore initial state. Update filter count. + if ( 0 === tags.length ) { + tags = ''; + section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show(); + section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide(); + } else { + section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length ); + section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide(); + section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show(); + } + + // Check whether tags have changed, and either load or queue them. + if ( ! _.isEqual( section.tags, tags ) ) { + if ( section.loading ) { + section.nextTags = tags; + } else { + if ( 'remote' === section.params.filter_type ) { + section.initializeNewQuery( section.term, tags ); + } else if ( 'local' === section.params.filter_type ) { + section.filterSearch( tags.join( ' ' ) ); + } + } + } + }, + + /** + * Reset the current query and load new results. + * + * @since 4.9.0 + * + * @param {string} newTerm - New term. + * @param {Array} newTags - New tags. + * @return {void} + */ + initializeNewQuery: function( newTerm, newTags ) { + var section = this; + + // Clear the controls in the section. + _.each( section.controls(), function( control ) { + control.container.remove(); + api.control.remove( control.id ); + }); + section.loaded = 0; + section.fullyLoaded = false; + section.screenshotQueue = null; + + // Run a new query, with loadThemes handling paging, etc. + if ( ! section.loading ) { + section.term = newTerm; + section.tags = newTags; + section.loadThemes(); + } else { + section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded. + section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded. + } + if ( ! section.expanded() ) { + section.expand(); // Expand the section if it isn't expanded. + } + }, + + /** + * Render control's screenshot if the control comes into view. + * + * @since 4.2.0 + * + * @return {void} + */ + renderScreenshots: function() { + var section = this; + + // Fill queue initially, or check for more if empty. + if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) { + + // Add controls that haven't had their screenshots rendered. + section.screenshotQueue = _.filter( section.controls(), function( control ) { + return ! control.screenshotRendered; + }); + } + + // Are all screenshots rendered (for now)? + if ( ! section.screenshotQueue.length ) { + return; + } + + section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) { + var $imageWrapper = control.container.find( '.theme-screenshot' ), + $image = $imageWrapper.find( 'img' ); + + if ( ! $image.length ) { + return false; + } + + if ( $image.is( ':hidden' ) ) { + return true; + } + + // Based on unveil.js. + var wt = section.$window.scrollTop(), + wb = wt + section.$window.height(), + et = $image.offset().top, + ih = $imageWrapper.height(), + eb = et + ih, + threshold = ih * 3, + inView = eb >= wt - threshold && et <= wb + threshold; + + if ( inView ) { + control.container.trigger( 'render-screenshot' ); + } + + // If the image is in view return false so it's cleared from the queue. + return ! inView; + } ); + }, + + /** + * Get visible count. + * + * @since 4.9.0 + * + * @return {number} Visible count. + */ + getVisibleCount: function() { + return this.contentContainer.find( 'li.customize-control:visible' ).length; + }, + + /** + * Update the number of themes in the section. + * + * @since 4.9.0 + * + * @return {void} + */ + updateCount: function( count ) { + var section = this, countEl, displayed; + + if ( ! count && 0 !== count ) { + count = section.getVisibleCount(); + } + + displayed = section.contentContainer.find( '.themes-displayed' ); + countEl = section.contentContainer.find( '.theme-count' ); + + if ( 0 === count ) { + countEl.text( '0' ); + } else { + + // Animate the count change for emphasis. + displayed.fadeOut( 180, function() { + countEl.text( count ); + displayed.fadeIn( 180 ); + } ); + wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) ); + } + }, + + /** + * Advance the modal to the next theme. + * + * @since 4.2.0 + * + * @return {void} + */ + nextTheme: function () { + var section = this; + if ( section.getNextTheme() ) { + section.showDetails( section.getNextTheme(), function() { + section.overlay.find( '.right' ).focus(); + } ); + } + }, + + /** + * Get the next theme model. + * + * @since 4.2.0 + * + * @return {wp.customize.ThemeControl|boolean} Next theme. + */ + getNextTheme: function () { + var section = this, control, nextControl, sectionControls, i; + control = api.control( section.params.action + '_theme_' + section.currentTheme ); + sectionControls = section.controls(); + i = _.indexOf( sectionControls, control ); + if ( -1 === i ) { + return false; + } + + nextControl = sectionControls[ i + 1 ]; + if ( ! nextControl ) { + return false; + } + return nextControl.params.theme; + }, + + /** + * Advance the modal to the previous theme. + * + * @since 4.2.0 + * @return {void} + */ + previousTheme: function () { + var section = this; + if ( section.getPreviousTheme() ) { + section.showDetails( section.getPreviousTheme(), function() { + section.overlay.find( '.left' ).focus(); + } ); + } + }, + + /** + * Get the previous theme model. + * + * @since 4.2.0 + * @return {wp.customize.ThemeControl|boolean} Previous theme. + */ + getPreviousTheme: function () { + var section = this, control, nextControl, sectionControls, i; + control = api.control( section.params.action + '_theme_' + section.currentTheme ); + sectionControls = section.controls(); + i = _.indexOf( sectionControls, control ); + if ( -1 === i ) { + return false; + } + + nextControl = sectionControls[ i - 1 ]; + if ( ! nextControl ) { + return false; + } + return nextControl.params.theme; + }, + + /** + * Disable buttons when we're viewing the first or last theme. + * + * @since 4.2.0 + * + * @return {void} + */ + updateLimits: function () { + if ( ! this.getNextTheme() ) { + this.overlay.find( '.right' ).addClass( 'disabled' ); + } + if ( ! this.getPreviousTheme() ) { + this.overlay.find( '.left' ).addClass( 'disabled' ); + } + }, + + /** + * Load theme preview. + * + * @since 4.7.0 + * @access public + * + * @deprecated + * @param {string} themeId Theme ID. + * @return {jQuery.promise} Promise. + */ + loadThemePreview: function( themeId ) { + return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId ); + }, + + /** + * Render & show the theme details for a given theme model. + * + * @since 4.2.0 + * + * @param {Object} theme - Theme. + * @param {Function} [callback] - Callback once the details have been shown. + * @return {void} + */ + showDetails: function ( theme, callback ) { + var section = this, panel = api.panel( 'themes' ); + section.currentTheme = theme.id; + section.overlay.html( section.template( theme ) ) + .fadeIn( 'fast' ) + .focus(); + + function disableSwitchButtons() { + return ! panel.canSwitchTheme( theme.id ); + } + + // Temporary special function since supplying SFTP credentials does not work yet. See #42184. + function disableInstallButtons() { + return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded; + } + + section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() ); + section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() ); + + section.$body.addClass( 'modal-open' ); + section.containFocus( section.overlay ); + section.updateLimits(); + wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) ); + if ( callback ) { + callback(); + } + }, + + /** + * Close the theme details modal. + * + * @since 4.2.0 + * + * @return {void} + */ + closeDetails: function () { + var section = this; + section.$body.removeClass( 'modal-open' ); + section.overlay.fadeOut( 'fast' ); + api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus(); + }, + + /** + * Keep tab focus within the theme details modal. + * + * @since 4.2.0 + * + * @param {jQuery} el - Element to contain focus. + * @return {void} + */ + containFocus: function( el ) { + var tabbables; + + el.on( 'keydown', function( event ) { + + // Return if it's not the tab key + // When navigating with prev/next focus is already handled. + if ( 9 !== event.keyCode ) { + return; + } + + // Uses jQuery UI to get the tabbable elements. + tabbables = $( ':tabbable', el ); + + // Keep focus within the overlay. + if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { + tabbables.first().focus(); + return false; + } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { + tabbables.last().focus(); + return false; + } + }); + } + }); + + api.OuterSection = api.Section.extend(/** @lends wp.customize.OuterSection.prototype */{ + + /** + * Class wp.customize.OuterSection. + * + * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so + * it would require custom handling. + * + * @constructs wp.customize.OuterSection + * @augments wp.customize.Section + * + * @since 4.9.0 + * + * @return {void} + */ + initialize: function() { + var section = this; + section.containerParent = '#customize-outer-theme-controls'; + section.containerPaneParent = '.customize-outer-pane-parent'; + api.Section.prototype.initialize.apply( section, arguments ); + }, + + /** + * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect + * on other sections and panels. + * + * @since 4.9.0 + * + * @param {boolean} expanded - The expanded state to transition to. + * @param {Object} [args] - Args. + * @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early. + * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed. + * @param {Object} [args.duration] - The duration for the animation. + */ + onChangeExpanded: function( expanded, args ) { + var section = this, + container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ), + content = section.contentContainer, + backBtn = content.find( '.customize-section-back' ), + sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(), + body = $( document.body ), + expand, panel; + + body.toggleClass( 'outer-section-open', expanded ); + section.container.toggleClass( 'open', expanded ); + section.container.removeClass( 'busy' ); + api.section.each( function( _section ) { + if ( 'outer' === _section.params.type && _section.id !== section.id ) { + _section.container.removeClass( 'open' ); + } + } ); + + if ( expanded && ! content.hasClass( 'open' ) ) { + + if ( args.unchanged ) { + expand = args.completeCallback; + } else { + expand = function() { + section._animateChangeExpanded( function() { + sectionTitle.attr( 'tabindex', '-1' ); + backBtn.attr( 'tabindex', '0' ); + + backBtn.trigger( 'focus' ); + content.css( 'top', '' ); + container.scrollTop( 0 ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + + content.addClass( 'open' ); + }.bind( this ); + } + + if ( section.panel() ) { + api.panel( section.panel() ).expand({ + duration: args.duration, + completeCallback: expand + }); + } else { + expand(); + } + + } else if ( ! expanded && content.hasClass( 'open' ) ) { + if ( section.panel() ) { + panel = api.panel( section.panel() ); + if ( panel.contentContainer.hasClass( 'skip-transition' ) ) { + panel.collapse(); + } + } + section._animateChangeExpanded( function() { + backBtn.attr( 'tabindex', '-1' ); + sectionTitle.attr( 'tabindex', '0' ); + + sectionTitle.trigger( 'focus' ); + content.css( 'top', '' ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + + content.removeClass( 'open' ); + + } else { + if ( args.completeCallback ) { + args.completeCallback(); + } + } + } + }); + + api.Panel = Container.extend(/** @lends wp.customize.Panel.prototype */{ + containerType: 'panel', + + /** + * @constructs wp.customize.Panel + * @augments wp.customize~Container + * + * @since 4.1.0 + * + * @param {string} id - The ID for the panel. + * @param {Object} options - Object containing one property: params. + * @param {string} options.title - Title shown when panel is collapsed and expanded. + * @param {string} [options.description] - Description shown at the top of the panel. + * @param {number} [options.priority=100] - The sort priority for the panel. + * @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor. + * @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used. + * @param {boolean} [options.active=true] - Whether the panel is active or not. + * @param {Object} [options.params] - Deprecated wrapper for the above properties. + */ + initialize: function ( id, options ) { + var panel = this, params; + params = options.params || options; + + // Look up the type if one was not supplied. + if ( ! params.type ) { + _.find( api.panelConstructor, function( Constructor, type ) { + if ( Constructor === panel.constructor ) { + params.type = type; + return true; + } + return false; + } ); + } + + Container.prototype.initialize.call( panel, id, params ); + + panel.embed(); + panel.deferred.embedded.done( function () { + panel.ready(); + }); + }, + + /** + * Embed the container in the DOM when any parent panel is ready. + * + * @since 4.1.0 + */ + embed: function () { + var panel = this, + container = $( '#customize-theme-controls' ), + parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable. + + if ( ! panel.headContainer.parent().is( parentContainer ) ) { + parentContainer.append( panel.headContainer ); + } + if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) { + container.append( panel.contentContainer ); + } + panel.renderContent(); + + panel.deferred.embedded.resolve(); + }, + + /** + * @since 4.1.0 + */ + attachEvents: function () { + var meta, panel = this; + + // Expand/Collapse accordion sections on click. + panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above. + + if ( ! panel.expanded() ) { + panel.expand(); + } + }); + + // Close panel. + panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above. + + if ( panel.expanded() ) { + panel.collapse(); + } + }); + + meta = panel.container.find( '.panel-meta:first' ); + + meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() { + if ( meta.hasClass( 'cannot-expand' ) ) { + return; + } + + var content = meta.find( '.customize-panel-description:first' ); + if ( meta.hasClass( 'open' ) ) { + meta.toggleClass( 'open' ); + content.slideUp( panel.defaultExpandedArguments.duration, function() { + content.trigger( 'toggled' ); + } ); + $( this ).attr( 'aria-expanded', false ); + } else { + content.slideDown( panel.defaultExpandedArguments.duration, function() { + content.trigger( 'toggled' ); + } ); + meta.toggleClass( 'open' ); + $( this ).attr( 'aria-expanded', true ); + } + }); + + }, + + /** + * Get the sections that are associated with this panel, sorted by their priority Value. + * + * @since 4.1.0 + * + * @return {Array} + */ + sections: function () { + return this._children( 'panel', 'section' ); + }, + + /** + * Return whether this panel has any active sections. + * + * @since 4.1.0 + * + * @return {boolean} Whether contextually active. + */ + isContextuallyActive: function () { + var panel = this, + sections = panel.sections(), + activeCount = 0; + _( sections ).each( function ( section ) { + if ( section.active() && section.isContextuallyActive() ) { + activeCount += 1; + } + } ); + return ( activeCount !== 0 ); + }, + + /** + * Update UI to reflect expanded state. + * + * @since 4.1.0 + * + * @param {boolean} expanded + * @param {Object} args + * @param {boolean} args.unchanged + * @param {Function} args.completeCallback + * @return {void} + */ + onChangeExpanded: function ( expanded, args ) { + + // Immediately call the complete callback if there were no changes. + if ( args.unchanged ) { + if ( args.completeCallback ) { + args.completeCallback(); + } + return; + } + + // Note: there is a second argument 'args' passed. + var panel = this, + accordionSection = panel.contentContainer, + overlay = accordionSection.closest( '.wp-full-overlay' ), + container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ), + topPanel = panel.headContainer.find( '.accordion-section-title' ), + backBtn = accordionSection.find( '.customize-panel-back' ), + childSections = panel.sections(), + skipTransition; + + if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) { + // Collapse any sibling sections/panels. + api.section.each( function ( section ) { + if ( panel.id !== section.panel() ) { + section.collapse( { duration: 0 } ); + } + }); + api.panel.each( function ( otherPanel ) { + if ( panel !== otherPanel ) { + otherPanel.collapse( { duration: 0 } ); + } + }); + + if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) { + accordionSection.addClass( 'current-panel skip-transition' ); + overlay.addClass( 'in-sub-panel' ); + + childSections[0].expand( { + completeCallback: args.completeCallback + } ); + } else { + panel._animateChangeExpanded( function() { + topPanel.attr( 'tabindex', '-1' ); + backBtn.attr( 'tabindex', '0' ); + + backBtn.trigger( 'focus' ); + accordionSection.css( 'top', '' ); + container.scrollTop( 0 ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + + accordionSection.addClass( 'current-panel' ); + overlay.addClass( 'in-sub-panel' ); + } + + api.state( 'expandedPanel' ).set( panel ); + + } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) { + skipTransition = accordionSection.hasClass( 'skip-transition' ); + if ( ! skipTransition ) { + panel._animateChangeExpanded( function() { + topPanel.attr( 'tabindex', '0' ); + backBtn.attr( 'tabindex', '-1' ); + + topPanel.focus(); + accordionSection.css( 'top', '' ); + + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + } else { + accordionSection.removeClass( 'skip-transition' ); + } + + overlay.removeClass( 'in-sub-panel' ); + accordionSection.removeClass( 'current-panel' ); + if ( panel === api.state( 'expandedPanel' ).get() ) { + api.state( 'expandedPanel' ).set( false ); + } + } + }, + + /** + * Render the panel from its JS template, if it exists. + * + * The panel's container must already exist in the DOM. + * + * @since 4.3.0 + */ + renderContent: function () { + var template, + panel = this; + + // Add the content to the container. + if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) { + template = wp.template( panel.templateSelector + '-content' ); + } else { + template = wp.template( 'customize-panel-default-content' ); + } + if ( template && panel.headContainer ) { + panel.contentContainer.html( template( _.extend( + { id: panel.id }, + panel.params + ) ) ); + } + } + }); + + api.ThemesPanel = api.Panel.extend(/** @lends wp.customize.ThemsPanel.prototype */{ + + /** + * Class wp.customize.ThemesPanel. + * + * Custom section for themes that displays without the customize preview. + * + * @constructs wp.customize.ThemesPanel + * @augments wp.customize.Panel + * + * @since 4.9.0 + * + * @param {string} id - The ID for the panel. + * @param {Object} options - Options. + * @return {void} + */ + initialize: function( id, options ) { + var panel = this; + panel.installingThemes = []; + api.Panel.prototype.initialize.call( panel, id, options ); + }, + + /** + * Determine whether a given theme can be switched to, or in general. + * + * @since 4.9.0 + * + * @param {string} [slug] - Theme slug. + * @return {boolean} Whether the theme can be switched to. + */ + canSwitchTheme: function canSwitchTheme( slug ) { + if ( slug && slug === api.settings.theme.stylesheet ) { + return true; + } + return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() ); + }, + + /** + * Attach events. + * + * @since 4.9.0 + * @return {void} + */ + attachEvents: function() { + var panel = this; + + // Attach regular panel events. + api.Panel.prototype.attachEvents.apply( panel ); + + // Temporary since supplying SFTP credentials does not work yet. See #42184. + if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) { + panel.notifications.add( new api.Notification( 'theme_install_unavailable', { + message: api.l10n.themeInstallUnavailable, + type: 'info', + dismissible: true + } ) ); + } + + function toggleDisabledNotifications() { + if ( panel.canSwitchTheme() ) { + panel.notifications.remove( 'theme_switch_unavailable' ); + } else { + panel.notifications.add( new api.Notification( 'theme_switch_unavailable', { + message: api.l10n.themePreviewUnavailable, + type: 'warning' + } ) ); + } + } + toggleDisabledNotifications(); + api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications ); + api.state( 'changesetStatus' ).bind( toggleDisabledNotifications ); + + // Collapse panel to customize the current theme. + panel.contentContainer.on( 'click', '.customize-theme', function() { + panel.collapse(); + }); + + // Toggle between filtering and browsing themes on mobile. + panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() { + $( '.wp-full-overlay' ).toggleClass( 'showing-themes' ); + }); + + // Install (and maybe preview) a theme. + panel.contentContainer.on( 'click', '.theme-install', function( event ) { + panel.installTheme( event ); + }); + + // Update a theme. Theme cards have the class, the details modal has the id. + panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) { + + // #update-theme is a link. + event.preventDefault(); + event.stopPropagation(); + + panel.updateTheme( event ); + }); + + // Delete a theme. + panel.contentContainer.on( 'click', '.delete-theme', function( event ) { + panel.deleteTheme( event ); + }); + + _.bindAll( panel, 'installTheme', 'updateTheme' ); + }, + + /** + * Update UI to reflect expanded state + * + * @since 4.9.0 + * + * @param {boolean} expanded - Expanded state. + * @param {Object} args - Args. + * @param {boolean} args.unchanged - Whether or not the state changed. + * @param {Function} args.completeCallback - Callback to execute when the animation completes. + * @return {void} + */ + onChangeExpanded: function( expanded, args ) { + var panel = this, overlay, sections, hasExpandedSection = false; + + // Expand/collapse the panel normally. + api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] ); + + // Immediately call the complete callback if there were no changes. + if ( args.unchanged ) { + if ( args.completeCallback ) { + args.completeCallback(); + } + return; + } + + overlay = panel.headContainer.closest( '.wp-full-overlay' ); + + if ( expanded ) { + overlay + .addClass( 'in-themes-panel' ) + .delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' ); + + _.delay( function() { + overlay.addClass( 'themes-panel-expanded' ); + }, 200 ); + + // Automatically open the first section (except on small screens), if one isn't already expanded. + if ( 600 < window.innerWidth ) { + sections = panel.sections(); + _.each( sections, function( section ) { + if ( section.expanded() ) { + hasExpandedSection = true; + } + } ); + if ( ! hasExpandedSection && sections.length > 0 ) { + sections[0].expand(); + } + } + } else { + overlay + .removeClass( 'in-themes-panel themes-panel-expanded' ) + .find( '.customize-themes-full-container' ).removeClass( 'animate' ); + } + }, + + /** + * Install a theme via wp.updates. + * + * @since 4.9.0 + * + * @param {jQuery.Event} event - Event. + * @return {jQuery.promise} Promise. + */ + installTheme: function( event ) { + var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request; + preview = $( event.target ).hasClass( 'preview' ); + + // Temporary since supplying SFTP credentials does not work yet. See #42184. + if ( api.settings.theme._filesystemCredentialsNeeded ) { + deferred.reject({ + errorCode: 'theme_install_unavailable' + }); + return deferred.promise(); + } + + // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset. + if ( ! panel.canSwitchTheme( slug ) ) { + deferred.reject({ + errorCode: 'theme_switch_unavailable' + }); + return deferred.promise(); + } + + // Theme is already being installed. + if ( _.contains( panel.installingThemes, slug ) ) { + deferred.reject({ + errorCode: 'theme_already_installing' + }); + return deferred.promise(); + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + onInstallSuccess = function( response ) { + var theme = false, themeControl; + if ( preview ) { + api.notifications.remove( 'theme_installing' ); + + panel.loadThemePreview( slug ); + + } else { + api.control.each( function( control ) { + if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) { + theme = control.params.theme; // Used below to add theme control. + control.rerenderAsInstalled( true ); + } + }); + + // Don't add the same theme more than once. + if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) { + deferred.resolve( response ); + return; + } + + // Add theme control to installed section. + theme.type = 'installed'; + themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, { + type: 'theme', + section: 'installed_themes', + theme: theme, + priority: 0 // Add all newly-installed themes to the top. + } ); + + api.control.add( themeControl ); + api.control( themeControl.id ).container.trigger( 'render-screenshot' ); + + // Close the details modal if it's open to the installed theme. + api.section.each( function( section ) { + if ( 'themes' === section.params.type ) { + if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere. + section.closeDetails(); + } + } + }); + } + deferred.resolve( response ); + }; + + panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again. + request = wp.updates.installTheme( { + slug: slug + } ); + + // Also preview the theme as the event is triggered on Install & Preview. + if ( preview ) { + api.notifications.add( new api.OverlayNotification( 'theme_installing', { + message: api.l10n.themeDownloading, + type: 'info', + loading: true + } ) ); + } + + request.done( onInstallSuccess ); + request.fail( function() { + api.notifications.remove( 'theme_installing' ); + } ); + + return deferred.promise(); + }, + + /** + * Load theme preview. + * + * @since 4.9.0 + * + * @param {string} themeId Theme ID. + * @return {jQuery.promise} Promise. + */ + loadThemePreview: function( themeId ) { + var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams; + + // Prevent loading a non-active theme preview when there is a drafted/scheduled changeset. + if ( ! panel.canSwitchTheme( themeId ) ) { + deferred.reject({ + errorCode: 'theme_switch_unavailable' + }); + return deferred.promise(); + } + + urlParser = document.createElement( 'a' ); + urlParser.href = location.href; + queryParams = _.extend( + api.utils.parseQueryString( urlParser.search.substr( 1 ) ), + { + theme: themeId, + changeset_uuid: api.settings.changeset.uuid, + 'return': api.settings.url['return'] + } + ); + + // Include autosaved param to load autosave revision without prompting user to restore it. + if ( ! api.state( 'saved' ).get() ) { + queryParams.customize_autosaved = 'on'; + } + + urlParser.search = $.param( queryParams ); + + // Update loading message. Everything else is handled by reloading the page. + api.notifications.add( new api.OverlayNotification( 'theme_previewing', { + message: api.l10n.themePreviewWait, + type: 'info', + loading: true + } ) ); + + onceProcessingComplete = function() { + var request; + if ( api.state( 'processing' ).get() > 0 ) { + return; + } + + api.state( 'processing' ).unbind( onceProcessingComplete ); + + request = api.requestChangesetUpdate( {}, { autosave: true } ); + request.done( function() { + deferred.resolve(); + $( window ).off( 'beforeunload.customize-confirm' ); + location.replace( urlParser.href ); + } ); + request.fail( function() { + + // @todo Show notification regarding failure. + api.notifications.remove( 'theme_previewing' ); + + deferred.reject(); + } ); + }; + + if ( 0 === api.state( 'processing' ).get() ) { + onceProcessingComplete(); + } else { + api.state( 'processing' ).bind( onceProcessingComplete ); + } + + return deferred.promise(); + }, + + /** + * Update a theme via wp.updates. + * + * @since 4.9.0 + * + * @param {jQuery.Event} event - Event. + * @return {void} + */ + updateTheme: function( event ) { + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).one( 'wp-theme-update-success', function( e, response ) { + + // Rerender the control to reflect the update. + api.control.each( function( control ) { + if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) { + control.params.theme.hasUpdate = false; + control.params.theme.version = response.newVersion; + setTimeout( function() { + control.rerenderAsInstalled( true ); + }, 2000 ); + } + }); + } ); + + wp.updates.updateTheme( { + slug: $( event.target ).closest( '.notice' ).data( 'slug' ) + } ); + }, + + /** + * Delete a theme via wp.updates. + * + * @since 4.9.0 + * + * @param {jQuery.Event} event - Event. + * @return {void} + */ + deleteTheme: function( event ) { + var theme, section; + theme = $( event.target ).data( 'slug' ); + section = api.section( 'installed_themes' ); + + event.preventDefault(); + + // Temporary since supplying SFTP credentials does not work yet. See #42184. + if ( api.settings.theme._filesystemCredentialsNeeded ) { + return; + } + + // Confirmation dialog for deleting a theme. + if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).one( 'wp-theme-delete-success', function() { + var control = api.control( 'installed_theme_' + theme ); + + // Remove theme control. + control.container.remove(); + api.control.remove( control.id ); + + // Update installed count. + section.loaded = section.loaded - 1; + section.updateCount(); + + // Rerender any other theme controls as uninstalled. + api.control.each( function( control ) { + if ( 'theme' === control.params.type && control.params.theme.id === theme ) { + control.rerenderAsInstalled( false ); + } + }); + } ); + + wp.updates.deleteTheme( { + slug: theme + } ); + + // Close modal and focus the section. + section.closeDetails(); + section.focus(); + } + }); + + api.Control = api.Class.extend(/** @lends wp.customize.Control.prototype */{ + defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, + + /** + * Default params. + * + * @since 4.9.0 + * @var {object} + */ + defaults: { + label: '', + description: '', + active: true, + priority: 10 + }, + + /** + * A Customizer Control. + * + * A control provides a UI element that allows a user to modify a Customizer Setting. + * + * @see PHP class WP_Customize_Control. + * + * @constructs wp.customize.Control + * @augments wp.customize.Class + * + * @borrows wp.customize~focus as this#focus + * @borrows wp.customize~Container#activate as this#activate + * @borrows wp.customize~Container#deactivate as this#deactivate + * @borrows wp.customize~Container#_toggleActive as this#_toggleActive + * + * @param {string} id - Unique identifier for the control instance. + * @param {Object} options - Options hash for the control instance. + * @param {Object} options.type - Type of control (e.g. text, radio, dropdown-pages, etc.) + * @param {string} [options.content] - The HTML content for the control or at least its container. This should normally be left blank and instead supplying a templateId. + * @param {string} [options.templateId] - Template ID for control's content. + * @param {string} [options.priority=10] - Order of priority to show the control within the section. + * @param {string} [options.active=true] - Whether the control is active. + * @param {string} options.section - The ID of the section the control belongs to. + * @param {mixed} [options.setting] - The ID of the main setting or an instance of this setting. + * @param {mixed} options.settings - An object with keys (e.g. default) that maps to setting IDs or Setting/Value objects, or an array of setting IDs or Setting/Value objects. + * @param {mixed} options.settings.default - The ID of the setting the control relates to. + * @param {string} options.settings.data - @todo Is this used? + * @param {string} options.label - Label. + * @param {string} options.description - Description. + * @param {number} [options.instanceNumber] - Order in which this instance was created in relation to other instances. + * @param {Object} [options.params] - Deprecated wrapper for the above properties. + * @return {void} + */ + initialize: function( id, options ) { + var control = this, deferredSettingIds = [], settings, gatherSettings; + + control.params = _.extend( + {}, + control.defaults, + control.params || {}, // In case subclass already defines. + options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat. + ); + + if ( ! api.Control.instanceCounter ) { + api.Control.instanceCounter = 0; + } + api.Control.instanceCounter++; + if ( ! control.params.instanceNumber ) { + control.params.instanceNumber = api.Control.instanceCounter; + } + + // Look up the type if one was not supplied. + if ( ! control.params.type ) { + _.find( api.controlConstructor, function( Constructor, type ) { + if ( Constructor === control.constructor ) { + control.params.type = type; + return true; + } + return false; + } ); + } + + if ( ! control.params.content ) { + control.params.content = $( '<li></li>', { + id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ), + 'class': 'customize-control customize-control-' + control.params.type + } ); + } + + control.id = id; + control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709. + if ( control.params.content ) { + control.container = $( control.params.content ); + } else { + control.container = $( control.selector ); // Likely dead, per above. See #28709. + } + + if ( control.params.templateId ) { + control.templateSelector = control.params.templateId; + } else { + control.templateSelector = 'customize-control-' + control.params.type + '-content'; + } + + control.deferred = _.extend( control.deferred || {}, { + embedded: new $.Deferred() + } ); + control.section = new api.Value(); + control.priority = new api.Value(); + control.active = new api.Value(); + control.activeArgumentsQueue = []; + control.notifications = new api.Notifications({ + alt: control.altNotice + }); + + control.elements = []; + + control.active.bind( function ( active ) { + var args = control.activeArgumentsQueue.shift(); + args = $.extend( {}, control.defaultActiveArguments, args ); + control.onChangeActive( active, args ); + } ); + + control.section.set( control.params.section ); + control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); + control.active.set( control.params.active ); + + api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); + + control.settings = {}; + + settings = {}; + if ( control.params.setting ) { + settings['default'] = control.params.setting; + } + _.extend( settings, control.params.settings ); + + // Note: Settings can be an array or an object, with values being either setting IDs or Setting (or Value) objects. + _.each( settings, function( value, key ) { + var setting; + if ( _.isObject( value ) && _.isFunction( value.extended ) && value.extended( api.Value ) ) { + control.settings[ key ] = value; + } else if ( _.isString( value ) ) { + setting = api( value ); + if ( setting ) { + control.settings[ key ] = setting; + } else { + deferredSettingIds.push( value ); + } + } + } ); + + gatherSettings = function() { + + // Fill-in all resolved settings. + _.each( settings, function ( settingId, key ) { + if ( ! control.settings[ key ] && _.isString( settingId ) ) { + control.settings[ key ] = api( settingId ); + } + } ); + + // Make sure settings passed as array gets associated with default. + if ( control.settings[0] && ! control.settings['default'] ) { + control.settings['default'] = control.settings[0]; + } + + // Identify the main setting. + control.setting = control.settings['default'] || null; + + control.linkElements(); // Link initial elements present in server-rendered content. + control.embed(); + }; + + if ( 0 === deferredSettingIds.length ) { + gatherSettings(); + } else { + api.apply( api, deferredSettingIds.concat( gatherSettings ) ); + } + + // After the control is embedded on the page, invoke the "ready" method. + control.deferred.embedded.done( function () { + control.linkElements(); // Link any additional elements after template is rendered by renderContent(). + control.setupNotifications(); + control.ready(); + }); + }, + + /** + * Link elements between settings and inputs. + * + * @since 4.7.0 + * @access public + * + * @return {void} + */ + linkElements: function () { + var control = this, nodes, radios, element; + + nodes = control.container.find( '[data-customize-setting-link], [data-customize-setting-key-link]' ); + radios = {}; + + nodes.each( function () { + var node = $( this ), name, setting; + + if ( node.data( 'customizeSettingLinked' ) ) { + return; + } + node.data( 'customizeSettingLinked', true ); // Prevent re-linking element. + + if ( node.is( ':radio' ) ) { + name = node.prop( 'name' ); + if ( radios[name] ) { + return; + } + + radios[name] = true; + node = nodes.filter( '[name="' + name + '"]' ); + } + + // Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key. + if ( node.data( 'customizeSettingLink' ) ) { + setting = api( node.data( 'customizeSettingLink' ) ); + } else if ( node.data( 'customizeSettingKeyLink' ) ) { + setting = control.settings[ node.data( 'customizeSettingKeyLink' ) ]; + } + + if ( setting ) { + element = new api.Element( node ); + control.elements.push( element ); + element.sync( setting ); + element.set( setting() ); + } + } ); + }, + + /** + * Embed the control into the page. + */ + embed: function () { + var control = this, + inject; + + // Watch for changes to the section state. + inject = function ( sectionId ) { + var parentContainer; + if ( ! sectionId ) { // @todo Allow a control to be embedded without a section, for instance a control embedded in the front end. + return; + } + // Wait for the section to be registered. + api.section( sectionId, function ( section ) { + // Wait for the section to be ready/initialized. + section.deferred.embedded.done( function () { + parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' ); + if ( ! control.container.parent().is( parentContainer ) ) { + parentContainer.append( control.container ); + } + control.renderContent(); + control.deferred.embedded.resolve(); + }); + }); + }; + control.section.bind( inject ); + inject( control.section.get() ); + }, + + /** + * Triggered when the control's markup has been injected into the DOM. + * + * @return {void} + */ + ready: function() { + var control = this, newItem; + if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) { + newItem = control.container.find( '.new-content-item' ); + newItem.hide(); // Hide in JS to preserve flex display when showing. + control.container.on( 'click', '.add-new-toggle', function( e ) { + $( e.currentTarget ).slideUp( 180 ); + newItem.slideDown( 180 ); + newItem.find( '.create-item-input' ).focus(); + }); + control.container.on( 'click', '.add-content', function() { + control.addNewPage(); + }); + control.container.on( 'keydown', '.create-item-input', function( e ) { + if ( 13 === e.which ) { // Enter. + control.addNewPage(); + } + }); + } + }, + + /** + * Get the element inside of a control's container that contains the validation error message. + * + * Control subclasses may override this to return the proper container to render notifications into. + * Injects the notification container for existing controls that lack the necessary container, + * including special handling for nav menu items and widgets. + * + * @since 4.6.0 + * @return {jQuery} Setting validation message element. + */ + getNotificationsContainerElement: function() { + var control = this, controlTitle, notificationsContainer; + + notificationsContainer = control.container.find( '.customize-control-notifications-container:first' ); + if ( notificationsContainer.length ) { + return notificationsContainer; + } + + notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' ); + + if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) { + control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer ); + } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) { + control.container.find( '.widget-inside:first' ).prepend( notificationsContainer ); + } else { + controlTitle = control.container.find( '.customize-control-title' ); + if ( controlTitle.length ) { + controlTitle.after( notificationsContainer ); + } else { + control.container.prepend( notificationsContainer ); + } + } + return notificationsContainer; + }, + + /** + * Set up notifications. + * + * @since 4.9.0 + * @return {void} + */ + setupNotifications: function() { + var control = this, renderNotificationsIfVisible, onSectionAssigned; + + // Add setting notifications to the control notification. + _.each( control.settings, function( setting ) { + if ( ! setting.notifications ) { + return; + } + setting.notifications.bind( 'add', function( settingNotification ) { + var params = _.extend( + {}, + settingNotification, + { + setting: setting.id + } + ); + control.notifications.add( new api.Notification( setting.id + ':' + settingNotification.code, params ) ); + } ); + setting.notifications.bind( 'remove', function( settingNotification ) { + control.notifications.remove( setting.id + ':' + settingNotification.code ); + } ); + } ); + + renderNotificationsIfVisible = function() { + var sectionId = control.section(); + if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) { + control.notifications.render(); + } + }; + + control.notifications.bind( 'rendered', function() { + var notifications = control.notifications.get(); + control.container.toggleClass( 'has-notifications', 0 !== notifications.length ); + control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length ); + } ); + + onSectionAssigned = function( newSectionId, oldSectionId ) { + if ( oldSectionId && api.section.has( oldSectionId ) ) { + api.section( oldSectionId ).expanded.unbind( renderNotificationsIfVisible ); + } + if ( newSectionId ) { + api.section( newSectionId, function( section ) { + section.expanded.bind( renderNotificationsIfVisible ); + renderNotificationsIfVisible(); + }); + } + }; + + control.section.bind( onSectionAssigned ); + onSectionAssigned( control.section.get() ); + control.notifications.bind( 'change', _.debounce( renderNotificationsIfVisible ) ); + }, + + /** + * Render notifications. + * + * Renders the `control.notifications` into the control's container. + * Control subclasses may override this method to do their own handling + * of rendering notifications. + * + * @deprecated in favor of `control.notifications.render()` + * @since 4.6.0 + * @this {wp.customize.Control} + */ + renderNotifications: function() { + var control = this, container, notifications, hasError = false; + + if ( 'undefined' !== typeof console && console.warn ) { + console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method.' ); + } + + container = control.getNotificationsContainerElement(); + if ( ! container || ! container.length ) { + return; + } + notifications = []; + control.notifications.each( function( notification ) { + notifications.push( notification ); + if ( 'error' === notification.type ) { + hasError = true; + } + } ); + + if ( 0 === notifications.length ) { + container.stop().slideUp( 'fast' ); + } else { + container.stop().slideDown( 'fast', null, function() { + $( this ).css( 'height', 'auto' ); + } ); + } + + if ( ! control.notificationsTemplate ) { + control.notificationsTemplate = wp.template( 'customize-control-notifications' ); + } + + control.container.toggleClass( 'has-notifications', 0 !== notifications.length ); + control.container.toggleClass( 'has-error', hasError ); + container.empty().append( + control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } ).trim() + ); + }, + + /** + * Normal controls do not expand, so just expand its parent + * + * @param {Object} [params] + */ + expand: function ( params ) { + api.section( this.section() ).expand( params ); + }, + + /* + * Documented using @borrows in the constructor. + */ + focus: focus, + + /** + * Update UI in response to a change in the control's active state. + * This does not change the active state, it merely handles the behavior + * for when it does change. + * + * @since 4.1.0 + * + * @param {boolean} active + * @param {Object} args + * @param {number} args.duration + * @param {Function} args.completeCallback + */ + onChangeActive: function ( active, args ) { + if ( args.unchanged ) { + if ( args.completeCallback ) { + args.completeCallback(); + } + return; + } + + if ( ! $.contains( document, this.container[0] ) ) { + // jQuery.fn.slideUp is not hiding an element if it is not in the DOM. + this.container.toggle( active ); + if ( args.completeCallback ) { + args.completeCallback(); + } + } else if ( active ) { + this.container.slideDown( args.duration, args.completeCallback ); + } else { + this.container.slideUp( args.duration, args.completeCallback ); + } + }, + + /** + * @deprecated 4.1.0 Use this.onChangeActive() instead. + */ + toggle: function ( active ) { + return this.onChangeActive( active, this.defaultActiveArguments ); + }, + + /* + * Documented using @borrows in the constructor + */ + activate: Container.prototype.activate, + + /* + * Documented using @borrows in the constructor + */ + deactivate: Container.prototype.deactivate, + + /* + * Documented using @borrows in the constructor + */ + _toggleActive: Container.prototype._toggleActive, + + // @todo This function appears to be dead code and can be removed. + dropdownInit: function() { + var control = this, + statuses = this.container.find('.dropdown-status'), + params = this.params, + toggleFreeze = false, + update = function( to ) { + if ( 'string' === typeof to && params.statuses && params.statuses[ to ] ) { + statuses.html( params.statuses[ to ] ).show(); + } else { + statuses.hide(); + } + }; + + // Support the .dropdown class to open/close complex elements. + this.container.on( 'click keydown', '.dropdown', function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + + event.preventDefault(); + + if ( ! toggleFreeze ) { + control.container.toggleClass( 'open' ); + } + + if ( control.container.hasClass( 'open' ) ) { + control.container.parent().parent().find( 'li.library-selected' ).focus(); + } + + // Don't want to fire focus and click at same time. + toggleFreeze = true; + setTimeout(function () { + toggleFreeze = false; + }, 400); + }); + + this.setting.bind( update ); + update( this.setting() ); + }, + + /** + * Render the control from its JS template, if it exists. + * + * The control's container must already exist in the DOM. + * + * @since 4.1.0 + */ + renderContent: function () { + var control = this, template, standardTypes, templateId, sectionId; + + standardTypes = [ + 'button', + 'checkbox', + 'date', + 'datetime-local', + 'email', + 'month', + 'number', + 'password', + 'radio', + 'range', + 'search', + 'select', + 'tel', + 'time', + 'text', + 'textarea', + 'week', + 'url' + ]; + + templateId = control.templateSelector; + + // Use default content template when a standard HTML type is used, + // there isn't a more specific template existing, and the control container is empty. + if ( templateId === 'customize-control-' + control.params.type + '-content' && + _.contains( standardTypes, control.params.type ) && + ! document.getElementById( 'tmpl-' + templateId ) && + 0 === control.container.children().length ) + { + templateId = 'customize-control-default-content'; + } + + // Replace the container element's content with the control. + if ( document.getElementById( 'tmpl-' + templateId ) ) { + template = wp.template( templateId ); + if ( template && control.container ) { + control.container.html( template( control.params ) ); + } + } + + // Re-render notifications after content has been re-rendered. + control.notifications.container = control.getNotificationsContainerElement(); + sectionId = control.section(); + if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) { + control.notifications.render(); + } + }, + + /** + * Add a new page to a dropdown-pages control reusing menus code for this. + * + * @since 4.7.0 + * @access private + * + * @return {void} + */ + addNewPage: function () { + var control = this, promise, toggle, container, input, title, select; + + if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) { + return; + } + + toggle = control.container.find( '.add-new-toggle' ); + container = control.container.find( '.new-content-item' ); + input = control.container.find( '.create-item-input' ); + title = input.val(); + select = control.container.find( 'select' ); + + if ( ! title ) { + input.addClass( 'invalid' ); + return; + } + + input.removeClass( 'invalid' ); + input.attr( 'disabled', 'disabled' ); + + // The menus functions add the page, publish when appropriate, + // and also add the new page to the dropdown-pages controls. + promise = api.Menus.insertAutoDraftPost( { + post_title: title, + post_type: 'page' + } ); + promise.done( function( data ) { + var availableItem, $content, itemTemplate; + + // Prepare the new page as an available menu item. + // See api.Menus.submitNew(). + availableItem = new api.Menus.AvailableItemModel( { + 'id': 'post-' + data.post_id, // Used for available menu item Backbone models. + 'title': title, + 'type': 'post_type', + 'type_label': api.Menus.data.l10n.page_label, + 'object': 'page', + 'object_id': data.post_id, + 'url': data.url + } ); + + // Add the new item to the list of available menu items. + api.Menus.availableMenuItemsPanel.collection.add( availableItem ); + $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' ); + itemTemplate = wp.template( 'available-menu-item' ); + $content.prepend( itemTemplate( availableItem.attributes ) ); + + // Focus the select control. + select.focus(); + control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting. + + // Reset the create page form. + container.slideUp( 180 ); + toggle.slideDown( 180 ); + } ); + promise.always( function() { + input.val( '' ).removeAttr( 'disabled' ); + } ); + } + }); + + /** + * A colorpicker control. + * + * @class wp.customize.ColorControl + * @augments wp.customize.Control + */ + api.ColorControl = api.Control.extend(/** @lends wp.customize.ColorControl.prototype */{ + ready: function() { + var control = this, + isHueSlider = this.params.mode === 'hue', + updating = false, + picker; + + if ( isHueSlider ) { + picker = this.container.find( '.color-picker-hue' ); + picker.val( control.setting() ).wpColorPicker({ + change: function( event, ui ) { + updating = true; + control.setting( ui.color.h() ); + updating = false; + } + }); + } else { + picker = this.container.find( '.color-picker-hex' ); + picker.val( control.setting() ).wpColorPicker({ + change: function() { + updating = true; + control.setting.set( picker.wpColorPicker( 'color' ) ); + updating = false; + }, + clear: function() { + updating = true; + control.setting.set( '' ); + updating = false; + } + }); + } + + control.setting.bind( function ( value ) { + // Bail if the update came from the control itself. + if ( updating ) { + return; + } + picker.val( value ); + picker.wpColorPicker( 'color', value ); + } ); + + // Collapse color picker when hitting Esc instead of collapsing the current section. + control.container.on( 'keydown', function( event ) { + var pickerContainer; + if ( 27 !== event.which ) { // Esc. + return; + } + pickerContainer = control.container.find( '.wp-picker-container' ); + if ( pickerContainer.hasClass( 'wp-picker-active' ) ) { + picker.wpColorPicker( 'close' ); + control.container.find( '.wp-color-result' ).focus(); + event.stopPropagation(); // Prevent section from being collapsed. + } + } ); + } + }); + + /** + * A control that implements the media modal. + * + * @class wp.customize.MediaControl + * @augments wp.customize.Control + */ + api.MediaControl = api.Control.extend(/** @lends wp.customize.MediaControl.prototype */{ + + /** + * When the control's DOM structure is ready, + * set up internal event bindings. + */ + ready: function() { + var control = this; + // Shortcut so that we don't have to use _.bind every time we add a callback. + _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' ); + + // Bind events, with delegation to facilitate re-rendering. + control.container.on( 'click keydown', '.upload-button', control.openFrame ); + control.container.on( 'click keydown', '.upload-button', control.pausePlayer ); + control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame ); + control.container.on( 'click keydown', '.default-button', control.restoreDefault ); + control.container.on( 'click keydown', '.remove-button', control.pausePlayer ); + control.container.on( 'click keydown', '.remove-button', control.removeFile ); + control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer ); + + // Resize the player controls when it becomes visible (ie when section is expanded). + api.section( control.section() ).container + .on( 'expanded', function() { + if ( control.player ) { + control.player.setControlsSize(); + } + }) + .on( 'collapsed', function() { + control.pausePlayer(); + }); + + /** + * Set attachment data and render content. + * + * Note that BackgroundImage.prototype.ready applies this ready method + * to itself. Since BackgroundImage is an UploadControl, the value + * is the attachment URL instead of the attachment ID. In this case + * we skip fetching the attachment data because we have no ID available, + * and it is the responsibility of the UploadControl to set the control's + * attachmentData before calling the renderContent method. + * + * @param {number|string} value Attachment + */ + function setAttachmentDataAndRenderContent( value ) { + var hasAttachmentData = $.Deferred(); + + if ( control.extended( api.UploadControl ) ) { + hasAttachmentData.resolve(); + } else { + value = parseInt( value, 10 ); + if ( _.isNaN( value ) || value <= 0 ) { + delete control.params.attachment; + hasAttachmentData.resolve(); + } else if ( control.params.attachment && control.params.attachment.id === value ) { + hasAttachmentData.resolve(); + } + } + + // Fetch the attachment data. + if ( 'pending' === hasAttachmentData.state() ) { + wp.media.attachment( value ).fetch().done( function() { + control.params.attachment = this.attributes; + hasAttachmentData.resolve(); + + // Send attachment information to the preview for possible use in `postMessage` transport. + wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes ); + } ); + } + + hasAttachmentData.done( function() { + control.renderContent(); + } ); + } + + // Ensure attachment data is initially set (for dynamically-instantiated controls). + setAttachmentDataAndRenderContent( control.setting() ); + + // Update the attachment data and re-render the control when the setting changes. + control.setting.bind( setAttachmentDataAndRenderContent ); + }, + + pausePlayer: function () { + this.player && this.player.pause(); + }, + + cleanupPlayer: function () { + this.player && wp.media.mixin.removePlayer( this.player ); + }, + + /** + * Open the media modal. + */ + openFrame: function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + + event.preventDefault(); + + if ( ! this.frame ) { + this.initFrame(); + } + + this.frame.open(); + }, + + /** + * Create a media modal select frame, and store it so the instance can be reused when needed. + */ + initFrame: function() { + this.frame = wp.media({ + button: { + text: this.params.button_labels.frame_button + }, + states: [ + new wp.media.controller.Library({ + title: this.params.button_labels.frame_title, + library: wp.media.query({ type: this.params.mime_type }), + multiple: false, + date: false + }) + ] + }); + + // When a file is selected, run a callback. + this.frame.on( 'select', this.select ); + }, + + /** + * Callback handler for when an attachment is selected in the media modal. + * Gets the selected image information, and sets it within the control. + */ + select: function() { + // Get the attachment from the modal frame. + var node, + attachment = this.frame.state().get( 'selection' ).first().toJSON(), + mejsSettings = window._wpmejsSettings || {}; + + this.params.attachment = attachment; + + // Set the Customizer setting; the callback takes care of rendering. + this.setting( attachment.id ); + node = this.container.find( 'audio, video' ).get(0); + + // Initialize audio/video previews. + if ( node ) { + this.player = new MediaElementPlayer( node, mejsSettings ); + } else { + this.cleanupPlayer(); + } + }, + + /** + * Reset the setting to the default value. + */ + restoreDefault: function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); + + this.params.attachment = this.params.defaultAttachment; + this.setting( this.params.defaultAttachment.url ); + }, + + /** + * Called when the "Remove" link is clicked. Empties the setting. + * + * @param {Object} event jQuery Event object + */ + removeFile: function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); + + this.params.attachment = {}; + this.setting( '' ); + this.renderContent(); // Not bound to setting change when emptying. + } + }); + + /** + * An upload control, which utilizes the media modal. + * + * @class wp.customize.UploadControl + * @augments wp.customize.MediaControl + */ + api.UploadControl = api.MediaControl.extend(/** @lends wp.customize.UploadControl.prototype */{ + + /** + * Callback handler for when an attachment is selected in the media modal. + * Gets the selected image information, and sets it within the control. + */ + select: function() { + // Get the attachment from the modal frame. + var node, + attachment = this.frame.state().get( 'selection' ).first().toJSON(), + mejsSettings = window._wpmejsSettings || {}; + + this.params.attachment = attachment; + + // Set the Customizer setting; the callback takes care of rendering. + this.setting( attachment.url ); + node = this.container.find( 'audio, video' ).get(0); + + // Initialize audio/video previews. + if ( node ) { + this.player = new MediaElementPlayer( node, mejsSettings ); + } else { + this.cleanupPlayer(); + } + }, + + // @deprecated + success: function() {}, + + // @deprecated + removerVisibility: function() {} + }); + + /** + * A control for uploading images. + * + * This control no longer needs to do anything more + * than what the upload control does in JS. + * + * @class wp.customize.ImageControl + * @augments wp.customize.UploadControl + */ + api.ImageControl = api.UploadControl.extend(/** @lends wp.customize.ImageControl.prototype */{ + // @deprecated + thumbnailSrc: function() {} + }); + + /** + * A control for uploading background images. + * + * @class wp.customize.BackgroundControl + * @augments wp.customize.UploadControl + */ + api.BackgroundControl = api.UploadControl.extend(/** @lends wp.customize.BackgroundControl.prototype */{ + + /** + * When the control's DOM structure is ready, + * set up internal event bindings. + */ + ready: function() { + api.UploadControl.prototype.ready.apply( this, arguments ); + }, + + /** + * Callback handler for when an attachment is selected in the media modal. + * Does an additional Ajax request for setting the background context. + */ + select: function() { + api.UploadControl.prototype.select.apply( this, arguments ); + + wp.ajax.post( 'custom-background-add', { + nonce: _wpCustomizeBackground.nonces.add, + wp_customize: 'on', + customize_theme: api.settings.theme.stylesheet, + attachment_id: this.params.attachment.id + } ); + } + }); + + /** + * A control for positioning a background image. + * + * @since 4.7.0 + * + * @class wp.customize.BackgroundPositionControl + * @augments wp.customize.Control + */ + api.BackgroundPositionControl = api.Control.extend(/** @lends wp.customize.BackgroundPositionControl.prototype */{ + + /** + * Set up control UI once embedded in DOM and settings are created. + * + * @since 4.7.0 + * @access public + */ + ready: function() { + var control = this, updateRadios; + + control.container.on( 'change', 'input[name="background-position"]', function() { + var position = $( this ).val().split( ' ' ); + control.settings.x( position[0] ); + control.settings.y( position[1] ); + } ); + + updateRadios = _.debounce( function() { + var x, y, radioInput, inputValue; + x = control.settings.x.get(); + y = control.settings.y.get(); + inputValue = String( x ) + ' ' + String( y ); + radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' ); + radioInput.trigger( 'click' ); + } ); + control.settings.x.bind( updateRadios ); + control.settings.y.bind( updateRadios ); + + updateRadios(); // Set initial UI. + } + } ); + + /** + * A control for selecting and cropping an image. + * + * @class wp.customize.CroppedImageControl + * @augments wp.customize.MediaControl + */ + api.CroppedImageControl = api.MediaControl.extend(/** @lends wp.customize.CroppedImageControl.prototype */{ + + /** + * Open the media modal to the library state. + */ + openFrame: function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + + this.initFrame(); + this.frame.setState( 'library' ).open(); + }, + + /** + * Create a media modal select frame, and store it so the instance can be reused when needed. + */ + initFrame: function() { + var l10n = _wpMediaViewsL10n; + + this.frame = wp.media({ + button: { + text: l10n.select, + close: false + }, + states: [ + new wp.media.controller.Library({ + title: this.params.button_labels.frame_title, + library: wp.media.query({ type: 'image' }), + multiple: false, + date: false, + priority: 20, + suggestedWidth: this.params.width, + suggestedHeight: this.params.height + }), + new wp.media.controller.CustomizeImageCropper({ + imgSelectOptions: this.calculateImageSelectOptions, + control: this + }) + ] + }); + + this.frame.on( 'select', this.onSelect, this ); + this.frame.on( 'cropped', this.onCropped, this ); + this.frame.on( 'skippedcrop', this.onSkippedCrop, this ); + }, + + /** + * After an image is selected in the media modal, switch to the cropper + * state if the image isn't the right size. + */ + onSelect: function() { + var attachment = this.frame.state().get( 'selection' ).first().toJSON(); + + if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) { + this.setImageFromAttachment( attachment ); + this.frame.close(); + } else { + this.frame.setState( 'cropper' ); + } + }, + + /** + * After the image has been cropped, apply the cropped image data to the setting. + * + * @param {Object} croppedImage Cropped attachment data. + */ + onCropped: function( croppedImage ) { + this.setImageFromAttachment( croppedImage ); + }, + + /** + * Returns a set of options, computed from the attached image data and + * control-specific data, to be fed to the imgAreaSelect plugin in + * wp.media.view.Cropper. + * + * @param {wp.media.model.Attachment} attachment + * @param {wp.media.controller.Cropper} controller + * @return {Object} Options + */ + calculateImageSelectOptions: function( attachment, controller ) { + var control = controller.get( 'control' ), + flexWidth = !! parseInt( control.params.flex_width, 10 ), + flexHeight = !! parseInt( control.params.flex_height, 10 ), + realWidth = attachment.get( 'width' ), + realHeight = attachment.get( 'height' ), + xInit = parseInt( control.params.width, 10 ), + yInit = parseInt( control.params.height, 10 ), + ratio = xInit / yInit, + xImg = xInit, + yImg = yInit, + x1, y1, imgSelectOptions; + + controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) ); + + if ( realWidth / realHeight > ratio ) { + yInit = realHeight; + xInit = yInit * ratio; + } else { + xInit = realWidth; + yInit = xInit / ratio; + } + + x1 = ( realWidth - xInit ) / 2; + y1 = ( realHeight - yInit ) / 2; + + imgSelectOptions = { + handles: true, + keys: true, + instance: true, + persistent: true, + imageWidth: realWidth, + imageHeight: realHeight, + minWidth: xImg > xInit ? xInit : xImg, + minHeight: yImg > yInit ? yInit : yImg, + x1: x1, + y1: y1, + x2: xInit + x1, + y2: yInit + y1 + }; + + if ( flexHeight === false && flexWidth === false ) { + imgSelectOptions.aspectRatio = xInit + ':' + yInit; + } + + if ( true === flexHeight ) { + delete imgSelectOptions.minHeight; + imgSelectOptions.maxWidth = realWidth; + } + + if ( true === flexWidth ) { + delete imgSelectOptions.minWidth; + imgSelectOptions.maxHeight = realHeight; + } + + return imgSelectOptions; + }, + + /** + * Return whether the image must be cropped, based on required dimensions. + * + * @param {boolean} flexW + * @param {boolean} flexH + * @param {number} dstW + * @param {number} dstH + * @param {number} imgW + * @param {number} imgH + * @return {boolean} + */ + mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) { + if ( true === flexW && true === flexH ) { + return false; + } + + if ( true === flexW && dstH === imgH ) { + return false; + } + + if ( true === flexH && dstW === imgW ) { + return false; + } + + if ( dstW === imgW && dstH === imgH ) { + return false; + } + + if ( imgW <= dstW ) { + return false; + } + + return true; + }, + + /** + * If cropping was skipped, apply the image data directly to the setting. + */ + onSkippedCrop: function() { + var attachment = this.frame.state().get( 'selection' ).first().toJSON(); + this.setImageFromAttachment( attachment ); + }, + + /** + * Updates the setting and re-renders the control UI. + * + * @param {Object} attachment + */ + setImageFromAttachment: function( attachment ) { + this.params.attachment = attachment; + + // Set the Customizer setting; the callback takes care of rendering. + this.setting( attachment.id ); + } + }); + + /** + * A control for selecting and cropping Site Icons. + * + * @class wp.customize.SiteIconControl + * @augments wp.customize.CroppedImageControl + */ + api.SiteIconControl = api.CroppedImageControl.extend(/** @lends wp.customize.SiteIconControl.prototype */{ + + /** + * Create a media modal select frame, and store it so the instance can be reused when needed. + */ + initFrame: function() { + var l10n = _wpMediaViewsL10n; + + this.frame = wp.media({ + button: { + text: l10n.select, + close: false + }, + states: [ + new wp.media.controller.Library({ + title: this.params.button_labels.frame_title, + library: wp.media.query({ type: 'image' }), + multiple: false, + date: false, + priority: 20, + suggestedWidth: this.params.width, + suggestedHeight: this.params.height + }), + new wp.media.controller.SiteIconCropper({ + imgSelectOptions: this.calculateImageSelectOptions, + control: this + }) + ] + }); + + this.frame.on( 'select', this.onSelect, this ); + this.frame.on( 'cropped', this.onCropped, this ); + this.frame.on( 'skippedcrop', this.onSkippedCrop, this ); + }, + + /** + * After an image is selected in the media modal, switch to the cropper + * state if the image isn't the right size. + */ + onSelect: function() { + var attachment = this.frame.state().get( 'selection' ).first().toJSON(), + controller = this; + + if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) { + wp.ajax.post( 'crop-image', { + nonce: attachment.nonces.edit, + id: attachment.id, + context: 'site-icon', + cropDetails: { + x1: 0, + y1: 0, + width: this.params.width, + height: this.params.height, + dst_width: this.params.width, + dst_height: this.params.height + } + } ).done( function( croppedImage ) { + controller.setImageFromAttachment( croppedImage ); + controller.frame.close(); + } ).fail( function() { + controller.frame.trigger('content:error:crop'); + } ); + } else { + this.frame.setState( 'cropper' ); + } + }, + + /** + * Updates the setting and re-renders the control UI. + * + * @param {Object} attachment + */ + setImageFromAttachment: function( attachment ) { + var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link, + icon; + + _.each( sizes, function( size ) { + if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) { + icon = attachment.sizes[ size ]; + } + } ); + + this.params.attachment = attachment; + + // Set the Customizer setting; the callback takes care of rendering. + this.setting( attachment.id ); + + if ( ! icon ) { + return; + } + + // Update the icon in-browser. + link = $( 'link[rel="icon"][sizes="32x32"]' ); + link.attr( 'href', icon.url ); + }, + + /** + * Called when the "Remove" link is clicked. Empties the setting. + * + * @param {Object} event jQuery Event object + */ + removeFile: function( event ) { + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); + + this.params.attachment = {}; + this.setting( '' ); + this.renderContent(); // Not bound to setting change when emptying. + $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default. + } + }); + + /** + * @class wp.customize.HeaderControl + * @augments wp.customize.Control + */ + api.HeaderControl = api.Control.extend(/** @lends wp.customize.HeaderControl.prototype */{ + ready: function() { + this.btnRemove = $('#customize-control-header_image .actions .remove'); + this.btnNew = $('#customize-control-header_image .actions .new'); + + _.bindAll(this, 'openMedia', 'removeImage'); + + this.btnNew.on( 'click', this.openMedia ); + this.btnRemove.on( 'click', this.removeImage ); + + api.HeaderTool.currentHeader = this.getInitialHeaderImage(); + + new api.HeaderTool.CurrentView({ + model: api.HeaderTool.currentHeader, + el: '#customize-control-header_image .current .container' + }); + + new api.HeaderTool.ChoiceListView({ + collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(), + el: '#customize-control-header_image .choices .uploaded .list' + }); + + new api.HeaderTool.ChoiceListView({ + collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(), + el: '#customize-control-header_image .choices .default .list' + }); + + api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([ + api.HeaderTool.UploadsList, + api.HeaderTool.DefaultsList + ]); + + // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme. + wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on'; + wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet; + }, + + /** + * Returns a new instance of api.HeaderTool.ImageModel based on the currently + * saved header image (if any). + * + * @since 4.2.0 + * + * @return {Object} Options + */ + getInitialHeaderImage: function() { + if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) { + return new api.HeaderTool.ImageModel(); + } + + // Get the matching uploaded image object. + var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) { + return ( imageObj.attachment_id === api.get().header_image_data.attachment_id ); + } ); + // Fall back to raw current header image. + if ( ! currentHeaderObject ) { + currentHeaderObject = { + url: api.get().header_image, + thumbnail_url: api.get().header_image, + attachment_id: api.get().header_image_data.attachment_id + }; + } + + return new api.HeaderTool.ImageModel({ + header: currentHeaderObject, + choice: currentHeaderObject.url.split( '/' ).pop() + }); + }, + + /** + * Returns a set of options, computed from the attached image data and + * theme-specific data, to be fed to the imgAreaSelect plugin in + * wp.media.view.Cropper. + * + * @param {wp.media.model.Attachment} attachment + * @param {wp.media.controller.Cropper} controller + * @return {Object} Options + */ + calculateImageSelectOptions: function(attachment, controller) { + var xInit = parseInt(_wpCustomizeHeader.data.width, 10), + yInit = parseInt(_wpCustomizeHeader.data.height, 10), + flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10), + flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10), + ratio, xImg, yImg, realHeight, realWidth, + imgSelectOptions; + + realWidth = attachment.get('width'); + realHeight = attachment.get('height'); + + this.headerImage = new api.HeaderTool.ImageModel(); + this.headerImage.set({ + themeWidth: xInit, + themeHeight: yInit, + themeFlexWidth: flexWidth, + themeFlexHeight: flexHeight, + imageWidth: realWidth, + imageHeight: realHeight + }); + + controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() ); + + ratio = xInit / yInit; + xImg = realWidth; + yImg = realHeight; + + if ( xImg / yImg > ratio ) { + yInit = yImg; + xInit = yInit * ratio; + } else { + xInit = xImg; + yInit = xInit / ratio; + } + + imgSelectOptions = { + handles: true, + keys: true, + instance: true, + persistent: true, + imageWidth: realWidth, + imageHeight: realHeight, + x1: 0, + y1: 0, + x2: xInit, + y2: yInit + }; + + if (flexHeight === false && flexWidth === false) { + imgSelectOptions.aspectRatio = xInit + ':' + yInit; + } + if (flexHeight === false ) { + imgSelectOptions.maxHeight = yInit; + } + if (flexWidth === false ) { + imgSelectOptions.maxWidth = xInit; + } + + return imgSelectOptions; + }, + + /** + * Sets up and opens the Media Manager in order to select an image. + * Depending on both the size of the image and the properties of the + * current theme, a cropping step after selection may be required or + * skippable. + * + * @param {event} event + */ + openMedia: function(event) { + var l10n = _wpMediaViewsL10n; + + event.preventDefault(); + + this.frame = wp.media({ + button: { + text: l10n.selectAndCrop, + close: false + }, + states: [ + new wp.media.controller.Library({ + title: l10n.chooseImage, + library: wp.media.query({ type: 'image' }), + multiple: false, + date: false, + priority: 20, + suggestedWidth: _wpCustomizeHeader.data.width, + suggestedHeight: _wpCustomizeHeader.data.height + }), + new wp.media.controller.Cropper({ + imgSelectOptions: this.calculateImageSelectOptions + }) + ] + }); + + this.frame.on('select', this.onSelect, this); + this.frame.on('cropped', this.onCropped, this); + this.frame.on('skippedcrop', this.onSkippedCrop, this); + + this.frame.open(); + }, + + /** + * After an image is selected in the media modal, + * switch to the cropper state. + */ + onSelect: function() { + this.frame.setState('cropper'); + }, + + /** + * After the image has been cropped, apply the cropped image data to the setting. + * + * @param {Object} croppedImage Cropped attachment data. + */ + onCropped: function(croppedImage) { + var url = croppedImage.url, + attachmentId = croppedImage.attachment_id, + w = croppedImage.width, + h = croppedImage.height; + this.setImageFromURL(url, attachmentId, w, h); + }, + + /** + * If cropping was skipped, apply the image data directly to the setting. + * + * @param {Object} selection + */ + onSkippedCrop: function(selection) { + var url = selection.get('url'), + w = selection.get('width'), + h = selection.get('height'); + this.setImageFromURL(url, selection.id, w, h); + }, + + /** + * Creates a new wp.customize.HeaderTool.ImageModel from provided + * header image data and inserts it into the user-uploaded headers + * collection. + * + * @param {string} url + * @param {number} attachmentId + * @param {number} width + * @param {number} height + */ + setImageFromURL: function(url, attachmentId, width, height) { + var choice, data = {}; + + data.url = url; + data.thumbnail_url = url; + data.timestamp = _.now(); + + if (attachmentId) { + data.attachment_id = attachmentId; + } + + if (width) { + data.width = width; + } + + if (height) { + data.height = height; + } + + choice = new api.HeaderTool.ImageModel({ + header: data, + choice: url.split('/').pop() + }); + api.HeaderTool.UploadsList.add(choice); + api.HeaderTool.currentHeader.set(choice.toJSON()); + choice.save(); + choice.importImage(); + }, + + /** + * Triggers the necessary events to deselect an image which was set as + * the currently selected one. + */ + removeImage: function() { + api.HeaderTool.currentHeader.trigger('hide'); + api.HeaderTool.CombinedList.trigger('control:removeImage'); + } + + }); + + /** + * wp.customize.ThemeControl + * + * @class wp.customize.ThemeControl + * @augments wp.customize.Control + */ + api.ThemeControl = api.Control.extend(/** @lends wp.customize.ThemeControl.prototype */{ + + touchDrag: false, + screenshotRendered: false, + + /** + * @since 4.2.0 + */ + ready: function() { + var control = this, panel = api.panel( 'themes' ); + + function disableSwitchButtons() { + return ! panel.canSwitchTheme( control.params.theme.id ); + } + + // Temporary special function since supplying SFTP credentials does not work yet. See #42184. + function disableInstallButtons() { + return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded; + } + function updateButtons() { + control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() ); + control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() ); + } + + api.state( 'selectedChangesetStatus' ).bind( updateButtons ); + api.state( 'changesetStatus' ).bind( updateButtons ); + updateButtons(); + + control.container.on( 'touchmove', '.theme', function() { + control.touchDrag = true; + }); + + // Bind details view trigger. + control.container.on( 'click keydown touchend', '.theme', function( event ) { + var section; + if ( api.utils.isKeydownButNotEnterEvent( event ) ) { + return; + } + + // Bail if the user scrolled on a touch device. + if ( control.touchDrag === true ) { + return control.touchDrag = false; + } + + // Prevent the modal from showing when the user clicks the action button. + if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) { + return; + } + + event.preventDefault(); // Keep this AFTER the key filter above. + section = api.section( control.section() ); + section.showDetails( control.params.theme, function() { + + // Temporary special function since supplying SFTP credentials does not work yet. See #42184. + if ( api.settings.theme._filesystemCredentialsNeeded ) { + section.overlay.find( '.theme-actions .delete-theme' ).remove(); + } + } ); + }); + + control.container.on( 'render-screenshot', function() { + var $screenshot = $( this ).find( 'img' ), + source = $screenshot.data( 'src' ); + + if ( source ) { + $screenshot.attr( 'src', source ); + } + control.screenshotRendered = true; + }); + }, + + /** + * Show or hide the theme based on the presence of the term in the title, description, tags, and author. + * + * @since 4.2.0 + * @param {Array} terms - An array of terms to search for. + * @return {boolean} Whether a theme control was activated or not. + */ + filter: function( terms ) { + var control = this, + matchCount = 0, + haystack = control.params.theme.name + ' ' + + control.params.theme.description + ' ' + + control.params.theme.tags + ' ' + + control.params.theme.author + ' '; + haystack = haystack.toLowerCase().replace( '-', ' ' ); + + // Back-compat for behavior in WordPress 4.2.0 to 4.8.X. + if ( ! _.isArray( terms ) ) { + terms = [ terms ]; + } + + // Always give exact name matches highest ranking. + if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) { + matchCount = 100; + } else { + + // Search for and weight (by 10) complete term matches. + matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 ); + + // Search for each term individually (as whole-word and partial match) and sum weighted match counts. + _.each( terms, function( term ) { + matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted. + matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing. + }); + + // Upper limit on match ranking. + if ( matchCount > 99 ) { + matchCount = 99; + } + } + + if ( 0 !== matchCount ) { + control.activate(); + control.params.priority = 101 - matchCount; // Sort results by match count. + return true; + } else { + control.deactivate(); // Hide control. + control.params.priority = 101; + return false; + } + }, + + /** + * Rerender the theme from its JS template with the installed type. + * + * @since 4.9.0 + * + * @return {void} + */ + rerenderAsInstalled: function( installed ) { + var control = this, section; + if ( installed ) { + control.params.theme.type = 'installed'; + } else { + section = api.section( control.params.section ); + control.params.theme.type = section.params.action; + } + control.renderContent(); // Replaces existing content. + control.container.trigger( 'render-screenshot' ); + } + }); + + /** + * Class wp.customize.CodeEditorControl + * + * @since 4.9.0 + * + * @class wp.customize.CodeEditorControl + * @augments wp.customize.Control + */ + api.CodeEditorControl = api.Control.extend(/** @lends wp.customize.CodeEditorControl.prototype */{ + + /** + * Initialize. + * + * @since 4.9.0 + * @param {string} id - Unique identifier for the control instance. + * @param {Object} options - Options hash for the control instance. + * @return {void} + */ + initialize: function( id, options ) { + var control = this; + control.deferred = _.extend( control.deferred || {}, { + codemirror: $.Deferred() + } ); + api.Control.prototype.initialize.call( control, id, options ); + + // Note that rendering is debounced so the props will be used when rendering happens after add event. + control.notifications.bind( 'add', function( notification ) { + + // Skip if control notification is not from setting csslint_error notification. + if ( notification.code !== control.setting.id + ':csslint_error' ) { + return; + } + + // Customize the template and behavior of csslint_error notifications. + notification.templateId = 'customize-code-editor-lint-error-notification'; + notification.render = (function( render ) { + return function() { + var li = render.call( this ); + li.find( 'input[type=checkbox]' ).on( 'click', function() { + control.setting.notifications.remove( 'csslint_error' ); + } ); + return li; + }; + })( notification.render ); + } ); + }, + + /** + * Initialize the editor when the containing section is ready and expanded. + * + * @since 4.9.0 + * @return {void} + */ + ready: function() { + var control = this; + if ( ! control.section() ) { + control.initEditor(); + return; + } + + // Wait to initialize editor until section is embedded and expanded. + api.section( control.section(), function( section ) { + section.deferred.embedded.done( function() { + var onceExpanded; + if ( section.expanded() ) { + control.initEditor(); + } else { + onceExpanded = function( isExpanded ) { + if ( isExpanded ) { + control.initEditor(); + section.expanded.unbind( onceExpanded ); + } + }; + section.expanded.bind( onceExpanded ); + } + } ); + } ); + }, + + /** + * Initialize editor. + * + * @since 4.9.0 + * @return {void} + */ + initEditor: function() { + var control = this, element, editorSettings = false; + + // Obtain editorSettings for instantiation. + if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) { + + // Obtain default editor settings. + editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {}; + editorSettings.codemirror = _.extend( + {}, + editorSettings.codemirror, + { + indentUnit: 2, + tabSize: 2 + } + ); + + // Merge editor_settings param on top of defaults. + if ( _.isObject( control.params.editor_settings ) ) { + _.each( control.params.editor_settings, function( value, key ) { + if ( _.isObject( value ) ) { + editorSettings[ key ] = _.extend( + {}, + editorSettings[ key ], + value + ); + } + } ); + } + } + + element = new api.Element( control.container.find( 'textarea' ) ); + control.elements.push( element ); + element.sync( control.setting ); + element.set( control.setting() ); + + if ( editorSettings ) { + control.initSyntaxHighlightingEditor( editorSettings ); + } else { + control.initPlainTextareaEditor(); + } + }, + + /** + * Make sure editor gets focused when control is focused. + * + * @since 4.9.0 + * @param {Object} [params] - Focus params. + * @param {Function} [params.completeCallback] - Function to call when expansion is complete. + * @return {void} + */ + focus: function( params ) { + var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback; + originalCompleteCallback = extendedParams.completeCallback; + extendedParams.completeCallback = function() { + if ( originalCompleteCallback ) { + originalCompleteCallback(); + } + if ( control.editor ) { + control.editor.codemirror.focus(); + } + }; + api.Control.prototype.focus.call( control, extendedParams ); + }, + + /** + * Initialize syntax-highlighting editor. + * + * @since 4.9.0 + * @param {Object} codeEditorSettings - Code editor settings. + * @return {void} + */ + initSyntaxHighlightingEditor: function( codeEditorSettings ) { + var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false; + + settings = _.extend( {}, codeEditorSettings, { + onTabNext: _.bind( control.onTabNext, control ), + onTabPrevious: _.bind( control.onTabPrevious, control ), + onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control ) + }); + + control.editor = wp.codeEditor.initialize( $textarea, settings ); + + // Improve the editor accessibility. + $( control.editor.codemirror.display.lineDiv ) + .attr({ + role: 'textbox', + 'aria-multiline': 'true', + 'aria-label': control.params.label, + 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4' + }); + + // Focus the editor when clicking on its label. + control.container.find( 'label' ).on( 'click', function() { + control.editor.codemirror.focus(); + }); + + /* + * When the CodeMirror instance changes, mirror to the textarea, + * where we have our "true" change event handler bound. + */ + control.editor.codemirror.on( 'change', function( codemirror ) { + suspendEditorUpdate = true; + $textarea.val( codemirror.getValue() ).trigger( 'change' ); + suspendEditorUpdate = false; + }); + + // Update CodeMirror when the setting is changed by another plugin. + control.setting.bind( function( value ) { + if ( ! suspendEditorUpdate ) { + control.editor.codemirror.setValue( value ); + } + }); + + // Prevent collapsing section when hitting Esc to tab out of editor. + control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) { + var escKeyCode = 27; + if ( escKeyCode === event.keyCode ) { + event.stopPropagation(); + } + }); + + control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] ); + }, + + /** + * Handle tabbing to the field after the editor. + * + * @since 4.9.0 + * @return {void} + */ + onTabNext: function onTabNext() { + var control = this, controls, controlIndex, section; + section = api.section( control.section() ); + controls = section.controls(); + controlIndex = controls.indexOf( control ); + if ( controls.length === controlIndex + 1 ) { + $( '#customize-footer-actions .collapse-sidebar' ).trigger( 'focus' ); + } else { + controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus(); + } + }, + + /** + * Handle tabbing to the field before the editor. + * + * @since 4.9.0 + * @return {void} + */ + onTabPrevious: function onTabPrevious() { + var control = this, controls, controlIndex, section; + section = api.section( control.section() ); + controls = section.controls(); + controlIndex = controls.indexOf( control ); + if ( 0 === controlIndex ) { + section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus(); + } else { + controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus(); + } + }, + + /** + * Update error notice. + * + * @since 4.9.0 + * @param {Array} errorAnnotations - Error annotations. + * @return {void} + */ + onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) { + var control = this, message; + control.setting.notifications.remove( 'csslint_error' ); + + if ( 0 !== errorAnnotations.length ) { + if ( 1 === errorAnnotations.length ) { + message = api.l10n.customCssError.singular.replace( '%d', '1' ); + } else { + message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) ); + } + control.setting.notifications.add( new api.Notification( 'csslint_error', { + message: message, + type: 'error' + } ) ); + } + }, + + /** + * Initialize plain-textarea editor when syntax highlighting is disabled. + * + * @since 4.9.0 + * @return {void} + */ + initPlainTextareaEditor: function() { + var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0]; + + $textarea.on( 'blur', function onBlur() { + $textarea.data( 'next-tab-blurs', false ); + } ); + + $textarea.on( 'keydown', function onKeydown( event ) { + var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27; + + if ( escKeyCode === event.keyCode ) { + if ( ! $textarea.data( 'next-tab-blurs' ) ) { + $textarea.data( 'next-tab-blurs', true ); + event.stopPropagation(); // Prevent collapsing the section. + } + return; + } + + // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed. + if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) { + return; + } + + // Prevent capturing Tab characters if Esc was pressed. + if ( $textarea.data( 'next-tab-blurs' ) ) { + return; + } + + selectionStart = textarea.selectionStart; + selectionEnd = textarea.selectionEnd; + value = textarea.value; + + if ( selectionStart >= 0 ) { + textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) ); + $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1; + } + + event.stopPropagation(); + event.preventDefault(); + }); + + control.deferred.codemirror.rejectWith( control ); + } + }); + + /** + * Class wp.customize.DateTimeControl. + * + * @since 4.9.0 + * @class wp.customize.DateTimeControl + * @augments wp.customize.Control + */ + api.DateTimeControl = api.Control.extend(/** @lends wp.customize.DateTimeControl.prototype */{ + + /** + * Initialize behaviors. + * + * @since 4.9.0 + * @return {void} + */ + ready: function ready() { + var control = this; + + control.inputElements = {}; + control.invalidDate = false; + + _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' ); + + if ( ! control.setting ) { + throw new Error( 'Missing setting' ); + } + + control.container.find( '.date-input' ).each( function() { + var input = $( this ), component, element; + component = input.data( 'component' ); + element = new api.Element( input ); + control.inputElements[ component ] = element; + control.elements.push( element ); + + // Add invalid date error once user changes (and has blurred the input). + input.on( 'change', function() { + if ( control.invalidDate ) { + control.notifications.add( new api.Notification( 'invalid_date', { + message: api.l10n.invalidDate + } ) ); + } + } ); + + // Remove the error immediately after validity change. + input.on( 'input', _.debounce( function() { + if ( ! control.invalidDate ) { + control.notifications.remove( 'invalid_date' ); + } + } ) ); + + // Add zero-padding when blurring field. + input.on( 'blur', _.debounce( function() { + if ( ! control.invalidDate ) { + control.populateDateInputs(); + } + } ) ); + } ); + + control.inputElements.month.bind( control.updateDaysForMonth ); + control.inputElements.year.bind( control.updateDaysForMonth ); + control.populateDateInputs(); + control.setting.bind( control.populateDateInputs ); + + // Start populating setting after inputs have been populated. + _.each( control.inputElements, function( element ) { + element.bind( control.populateSetting ); + } ); + }, + + /** + * Parse datetime string. + * + * @since 4.9.0 + * + * @param {string} datetime - Date/Time string. Accepts Y-m-d[ H:i[:s]] format. + * @return {Object|null} Returns object containing date components or null if parse error. + */ + parseDateTime: function parseDateTime( datetime ) { + var control = this, matches, date, midDayHour = 12; + + if ( datetime ) { + matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ ); + } + + if ( ! matches ) { + return null; + } + + matches.shift(); + + date = { + year: matches.shift(), + month: matches.shift(), + day: matches.shift(), + hour: matches.shift() || '00', + minute: matches.shift() || '00', + second: matches.shift() || '00' + }; + + if ( control.params.includeTime && control.params.twelveHourFormat ) { + date.hour = parseInt( date.hour, 10 ); + date.meridian = date.hour >= midDayHour ? 'pm' : 'am'; + date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour ); + delete date.second; // @todo Why only if twelveHourFormat? + } + + return date; + }, + + /** + * Validates if input components have valid date and time. + * + * @since 4.9.0 + * @return {boolean} If date input fields has error. + */ + validateInputs: function validateInputs() { + var control = this, components, validityInput; + + control.invalidDate = false; + + components = [ 'year', 'day' ]; + if ( control.params.includeTime ) { + components.push( 'hour', 'minute' ); + } + + _.find( components, function( component ) { + var element, max, min, value; + + element = control.inputElements[ component ]; + validityInput = element.element.get( 0 ); + max = parseInt( element.element.attr( 'max' ), 10 ); + min = parseInt( element.element.attr( 'min' ), 10 ); + value = parseInt( element(), 10 ); + control.invalidDate = isNaN( value ) || value > max || value < min; + + if ( ! control.invalidDate ) { + validityInput.setCustomValidity( '' ); + } + + return control.invalidDate; + } ); + + if ( control.inputElements.meridian && ! control.invalidDate ) { + validityInput = control.inputElements.meridian.element.get( 0 ); + if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) { + control.invalidDate = true; + } else { + validityInput.setCustomValidity( '' ); + } + } + + if ( control.invalidDate ) { + validityInput.setCustomValidity( api.l10n.invalidValue ); + } else { + validityInput.setCustomValidity( '' ); + } + if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) { + _.result( validityInput, 'reportValidity' ); + } + + return control.invalidDate; + }, + + /** + * Updates number of days according to the month and year selected. + * + * @since 4.9.0 + * @return {void} + */ + updateDaysForMonth: function updateDaysForMonth() { + var control = this, daysInMonth, year, month, day; + + month = parseInt( control.inputElements.month(), 10 ); + year = parseInt( control.inputElements.year(), 10 ); + day = parseInt( control.inputElements.day(), 10 ); + + if ( month && year ) { + daysInMonth = new Date( year, month, 0 ).getDate(); + control.inputElements.day.element.attr( 'max', daysInMonth ); + + if ( day > daysInMonth ) { + control.inputElements.day( String( daysInMonth ) ); + } + } + }, + + /** + * Populate setting value from the inputs. + * + * @since 4.9.0 + * @return {boolean} If setting updated. + */ + populateSetting: function populateSetting() { + var control = this, date; + + if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) { + return false; + } + + date = control.convertInputDateToString(); + control.setting.set( date ); + return true; + }, + + /** + * Converts input values to string in Y-m-d H:i:s format. + * + * @since 4.9.0 + * @return {string} Date string. + */ + convertInputDateToString: function convertInputDateToString() { + var control = this, date = '', dateFormat, hourInTwentyFourHourFormat, + getElementValue, pad; + + pad = function( number, padding ) { + var zeros; + if ( String( number ).length < padding ) { + zeros = padding - String( number ).length; + number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number ); + } + return number; + }; + + getElementValue = function( component ) { + var value = parseInt( control.inputElements[ component ].get(), 10 ); + + if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) { + value = pad( value, 2 ); + } else if ( 'year' === component ) { + value = pad( value, 4 ); + } + return value; + }; + + dateFormat = [ 'year', '-', 'month', '-', 'day' ]; + if ( control.params.includeTime ) { + hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour(); + dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] ); + } + + _.each( dateFormat, function( component ) { + date += control.inputElements[ component ] ? getElementValue( component ) : component; + } ); + + return date; + }, + + /** + * Check if the date is in the future. + * + * @since 4.9.0 + * @return {boolean} True if future date. + */ + isFutureDate: function isFutureDate() { + var control = this; + return 0 < api.utils.getRemainingTime( control.convertInputDateToString() ); + }, + + /** + * Convert hour in twelve hour format to twenty four hour format. + * + * @since 4.9.0 + * @param {string} hourInTwelveHourFormat - Hour in twelve hour format. + * @param {string} meridian - Either 'am' or 'pm'. + * @return {string} Hour in twenty four hour format. + */ + convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) { + var hourInTwentyFourHourFormat, hour, midDayHour = 12; + + hour = parseInt( hourInTwelveHourFormat, 10 ); + if ( isNaN( hour ) ) { + return ''; + } + + if ( 'pm' === meridian && hour < midDayHour ) { + hourInTwentyFourHourFormat = hour + midDayHour; + } else if ( 'am' === meridian && midDayHour === hour ) { + hourInTwentyFourHourFormat = hour - midDayHour; + } else { + hourInTwentyFourHourFormat = hour; + } + + return String( hourInTwentyFourHourFormat ); + }, + + /** + * Populates date inputs in date fields. + * + * @since 4.9.0 + * @return {boolean} Whether the inputs were populated. + */ + populateDateInputs: function populateDateInputs() { + var control = this, parsed; + + parsed = control.parseDateTime( control.setting.get() ); + + if ( ! parsed ) { + return false; + } + + _.each( control.inputElements, function( element, component ) { + var value = parsed[ component ]; // This will be zero-padded string. + + // Set month and meridian regardless of focused state since they are dropdowns. + if ( 'month' === component || 'meridian' === component ) { + + // Options in dropdowns are not zero-padded. + value = value.replace( /^0/, '' ); + + element.set( value ); + } else { + + value = parseInt( value, 10 ); + if ( ! element.element.is( document.activeElement ) ) { + + // Populate element with zero-padded value if not focused. + element.set( parsed[ component ] ); + } else if ( value !== parseInt( element(), 10 ) ) { + + // Forcibly update the value if its underlying value changed, regardless of zero-padding. + element.set( String( value ) ); + } + } + } ); + + return true; + }, + + /** + * Toggle future date notification for date control. + * + * @since 4.9.0 + * @param {boolean} notify Add or remove the notification. + * @return {wp.customize.DateTimeControl} + */ + toggleFutureDateNotification: function toggleFutureDateNotification( notify ) { + var control = this, notificationCode, notification; + + notificationCode = 'not_future_date'; + + if ( notify ) { + notification = new api.Notification( notificationCode, { + type: 'error', + message: api.l10n.futureDateError + } ); + control.notifications.add( notification ); + } else { + control.notifications.remove( notificationCode ); + } + + return control; + } + }); + + /** + * Class PreviewLinkControl. + * + * @since 4.9.0 + * @class wp.customize.PreviewLinkControl + * @augments wp.customize.Control + */ + api.PreviewLinkControl = api.Control.extend(/** @lends wp.customize.PreviewLinkControl.prototype */{ + + defaults: _.extend( {}, api.Control.prototype.defaults, { + templateId: 'customize-preview-link-control' + } ), + + /** + * Initialize behaviors. + * + * @since 4.9.0 + * @return {void} + */ + ready: function ready() { + var control = this, element, component, node, url, input, button; + + _.bindAll( control, 'updatePreviewLink' ); + + if ( ! control.setting ) { + control.setting = new api.Value(); + } + + control.previewElements = {}; + + control.container.find( '.preview-control-element' ).each( function() { + node = $( this ); + component = node.data( 'component' ); + element = new api.Element( node ); + control.previewElements[ component ] = element; + control.elements.push( element ); + } ); + + url = control.previewElements.url; + input = control.previewElements.input; + button = control.previewElements.button; + + input.link( control.setting ); + url.link( control.setting ); + + url.bind( function( value ) { + url.element.parent().attr( { + href: value, + target: api.settings.changeset.uuid + } ); + } ); + + api.bind( 'ready', control.updatePreviewLink ); + api.state( 'saved' ).bind( control.updatePreviewLink ); + api.state( 'changesetStatus' ).bind( control.updatePreviewLink ); + api.state( 'activated' ).bind( control.updatePreviewLink ); + api.previewer.previewUrl.bind( control.updatePreviewLink ); + + button.element.on( 'click', function( event ) { + event.preventDefault(); + if ( control.setting() ) { + input.element.select(); + document.execCommand( 'copy' ); + button( button.element.data( 'copied-text' ) ); + } + } ); + + url.element.parent().on( 'click', function( event ) { + if ( $( this ).hasClass( 'disabled' ) ) { + event.preventDefault(); + } + } ); + + button.element.on( 'mouseenter', function() { + if ( control.setting() ) { + button( button.element.data( 'copy-text' ) ); + } + } ); + }, + + /** + * Updates Preview Link + * + * @since 4.9.0 + * @return {void} + */ + updatePreviewLink: function updatePreviewLink() { + var control = this, unsavedDirtyValues; + + unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get(); + + control.toggleSaveNotification( unsavedDirtyValues ); + control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues ); + control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues ); + control.setting.set( api.previewer.getFrontendPreviewUrl() ); + }, + + /** + * Toggles save notification. + * + * @since 4.9.0 + * @param {boolean} notify Add or remove notification. + * @return {void} + */ + toggleSaveNotification: function toggleSaveNotification( notify ) { + var control = this, notificationCode, notification; + + notificationCode = 'changes_not_saved'; + + if ( notify ) { + notification = new api.Notification( notificationCode, { + type: 'info', + message: api.l10n.saveBeforeShare + } ); + control.notifications.add( notification ); + } else { + control.notifications.remove( notificationCode ); + } + } + }); + + /** + * Change objects contained within the main customize object to Settings. + * + * @alias wp.customize.defaultConstructor + */ + api.defaultConstructor = api.Setting; + + /** + * Callback for resolved controls. + * + * @callback wp.customize.deferredControlsCallback + * @param {wp.customize.Control[]} controls Resolved controls. + */ + + /** + * Collection of all registered controls. + * + * @alias wp.customize.control + * + * @since 3.4.0 + * + * @type {Function} + * @param {...string} ids - One or more ids for controls to obtain. + * @param {deferredControlsCallback} [callback] - Function called when all supplied controls exist. + * @return {wp.customize.Control|undefined|jQuery.promise} Control instance or undefined (if function called with one id param), + * or promise resolving to requested controls. + * + * @example <caption>Loop over all registered controls.</caption> + * wp.customize.control.each( function( control ) { ... } ); + * + * @example <caption>Getting `background_color` control instance.</caption> + * control = wp.customize.control( 'background_color' ); + * + * @example <caption>Check if control exists.</caption> + * hasControl = wp.customize.control.has( 'background_color' ); + * + * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption> + * wp.customize.control( 'background_color', function( control ) { ... } ); + * + * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption> + * promise = wp.customize.control( 'blogname', 'blogdescription' ); + * promise.done( function( titleControl, taglineControl ) { ... } ); + * + * @example <caption>Get title and tagline controls when they both exist, using callback.</caption> + * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } ); + * + * @example <caption>Getting setting value for `background_color` control.</caption> + * value = wp.customize.control( 'background_color ').setting.get(); + * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same. + * + * @example <caption>Add new control for site title.</caption> + * wp.customize.control.add( new wp.customize.Control( 'other_blogname', { + * setting: 'blogname', + * type: 'text', + * label: 'Site title', + * section: 'other_site_identify' + * } ) ); + * + * @example <caption>Remove control.</caption> + * wp.customize.control.remove( 'other_blogname' ); + * + * @example <caption>Listen for control being added.</caption> + * wp.customize.control.bind( 'add', function( addedControl ) { ... } ) + * + * @example <caption>Listen for control being removed.</caption> + * wp.customize.control.bind( 'removed', function( removedControl ) { ... } ) + */ + api.control = new api.Values({ defaultConstructor: api.Control }); + + /** + * Callback for resolved sections. + * + * @callback wp.customize.deferredSectionsCallback + * @param {wp.customize.Section[]} sections Resolved sections. + */ + + /** + * Collection of all registered sections. + * + * @alias wp.customize.section + * + * @since 3.4.0 + * + * @type {Function} + * @param {...string} ids - One or more ids for sections to obtain. + * @param {deferredSectionsCallback} [callback] - Function called when all supplied sections exist. + * @return {wp.customize.Section|undefined|jQuery.promise} Section instance or undefined (if function called with one id param), + * or promise resolving to requested sections. + * + * @example <caption>Loop over all registered sections.</caption> + * wp.customize.section.each( function( section ) { ... } ) + * + * @example <caption>Getting `title_tagline` section instance.</caption> + * section = wp.customize.section( 'title_tagline' ) + * + * @example <caption>Expand dynamically-created section when it exists.</caption> + * wp.customize.section( 'dynamically_created', function( section ) { + * section.expand(); + * } ); + * + * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances. + */ + api.section = new api.Values({ defaultConstructor: api.Section }); + + /** + * Callback for resolved panels. + * + * @callback wp.customize.deferredPanelsCallback + * @param {wp.customize.Panel[]} panels Resolved panels. + */ + + /** + * Collection of all registered panels. + * + * @alias wp.customize.panel + * + * @since 4.0.0 + * + * @type {Function} + * @param {...string} ids - One or more ids for panels to obtain. + * @param {deferredPanelsCallback} [callback] - Function called when all supplied panels exist. + * @return {wp.customize.Panel|undefined|jQuery.promise} Panel instance or undefined (if function called with one id param), + * or promise resolving to requested panels. + * + * @example <caption>Loop over all registered panels.</caption> + * wp.customize.panel.each( function( panel ) { ... } ) + * + * @example <caption>Getting nav_menus panel instance.</caption> + * panel = wp.customize.panel( 'nav_menus' ); + * + * @example <caption>Expand dynamically-created panel when it exists.</caption> + * wp.customize.panel( 'dynamically_created', function( panel ) { + * panel.expand(); + * } ); + * + * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances. + */ + api.panel = new api.Values({ defaultConstructor: api.Panel }); + + /** + * Callback for resolved notifications. + * + * @callback wp.customize.deferredNotificationsCallback + * @param {wp.customize.Notification[]} notifications Resolved notifications. + */ + + /** + * Collection of all global notifications. + * + * @alias wp.customize.notifications + * + * @since 4.9.0 + * + * @type {Function} + * @param {...string} codes - One or more codes for notifications to obtain. + * @param {deferredNotificationsCallback} [callback] - Function called when all supplied notifications exist. + * @return {wp.customize.Notification|undefined|jQuery.promise} Notification instance or undefined (if function called with one code param), + * or promise resolving to requested notifications. + * + * @example <caption>Check if existing notification</caption> + * exists = wp.customize.notifications.has( 'a_new_day_arrived' ); + * + * @example <caption>Obtain existing notification</caption> + * notification = wp.customize.notifications( 'a_new_day_arrived' ); + * + * @example <caption>Obtain notification that may not exist yet.</caption> + * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } ); + * + * @example <caption>Add a warning notification.</caption> + * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', { + * type: 'warning', + * message: 'Midnight has almost arrived!', + * dismissible: true + * } ) ); + * + * @example <caption>Remove a notification.</caption> + * wp.customize.notifications.remove( 'a_new_day_arrived' ); + * + * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances. + */ + api.notifications = new api.Notifications(); + + api.PreviewFrame = api.Messenger.extend(/** @lends wp.customize.PreviewFrame.prototype */{ + sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity. + + /** + * An object that fetches a preview in the background of the document, which + * allows for seamless replacement of an existing preview. + * + * @constructs wp.customize.PreviewFrame + * @augments wp.customize.Messenger + * + * @param {Object} params.container + * @param {Object} params.previewUrl + * @param {Object} params.query + * @param {Object} options + */ + initialize: function( params, options ) { + var deferred = $.Deferred(); + + /* + * Make the instance of the PreviewFrame the promise object + * so other objects can easily interact with it. + */ + deferred.promise( this ); + + this.container = params.container; + + $.extend( params, { channel: api.PreviewFrame.uuid() }); + + api.Messenger.prototype.initialize.call( this, params, options ); + + this.add( 'previewUrl', params.previewUrl ); + + this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() }); + + this.run( deferred ); + }, + + /** + * Run the preview request. + * + * @param {Object} deferred jQuery Deferred object to be resolved with + * the request. + */ + run: function( deferred ) { + var previewFrame = this, + loaded = false, + ready = false, + readyData = null, + hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized, + urlParser, + params, + form; + + if ( previewFrame._ready ) { + previewFrame.unbind( 'ready', previewFrame._ready ); + } + + previewFrame._ready = function( data ) { + ready = true; + readyData = data; + previewFrame.container.addClass( 'iframe-ready' ); + if ( ! data ) { + return; + } + + if ( loaded ) { + deferred.resolveWith( previewFrame, [ data ] ); + } + }; + + previewFrame.bind( 'ready', previewFrame._ready ); + + urlParser = document.createElement( 'a' ); + urlParser.href = previewFrame.previewUrl(); + + params = _.extend( + api.utils.parseQueryString( urlParser.search.substr( 1 ) ), + { + customize_changeset_uuid: previewFrame.query.customize_changeset_uuid, + customize_theme: previewFrame.query.customize_theme, + customize_messenger_channel: previewFrame.query.customize_messenger_channel + } + ); + if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) { + params.customize_autosaved = 'on'; + } + + urlParser.search = $.param( params ); + previewFrame.iframe = $( '<iframe />', { + title: api.l10n.previewIframeTitle, + name: 'customize-' + previewFrame.channel() + } ); + previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149. + previewFrame.iframe.attr( 'sandbox', 'allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts' ); + + if ( ! hasPendingChangesetUpdate ) { + previewFrame.iframe.attr( 'src', urlParser.href ); + } else { + previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes. + } + + previewFrame.iframe.appendTo( previewFrame.container ); + previewFrame.targetWindow( previewFrame.iframe[0].contentWindow ); + + /* + * Submit customized data in POST request to preview frame window since + * there are setting value changes not yet written to changeset. + */ + if ( hasPendingChangesetUpdate ) { + form = $( '<form>', { + action: urlParser.href, + target: previewFrame.iframe.attr( 'name' ), + method: 'post', + hidden: 'hidden' + } ); + form.append( $( '<input>', { + type: 'hidden', + name: '_method', + value: 'GET' + } ) ); + _.each( previewFrame.query, function( value, key ) { + form.append( $( '<input>', { + type: 'hidden', + name: key, + value: value + } ) ); + } ); + previewFrame.container.append( form ); + form.trigger( 'submit' ); + form.remove(); // No need to keep the form around after submitted. + } + + previewFrame.bind( 'iframe-loading-error', function( error ) { + previewFrame.iframe.remove(); + + // Check if the user is not logged in. + if ( 0 === error ) { + previewFrame.login( deferred ); + return; + } + + // Check for cheaters. + if ( -1 === error ) { + deferred.rejectWith( previewFrame, [ 'cheatin' ] ); + return; + } + + deferred.rejectWith( previewFrame, [ 'request failure' ] ); + } ); + + previewFrame.iframe.one( 'load', function() { + loaded = true; + + if ( ready ) { + deferred.resolveWith( previewFrame, [ readyData ] ); + } else { + setTimeout( function() { + deferred.rejectWith( previewFrame, [ 'ready timeout' ] ); + }, previewFrame.sensitivity ); + } + }); + }, + + login: function( deferred ) { + var self = this, + reject; + + reject = function() { + deferred.rejectWith( self, [ 'logged out' ] ); + }; + + if ( this.triedLogin ) { + return reject(); + } + + // Check if we have an admin cookie. + $.get( api.settings.url.ajax, { + action: 'logged-in' + }).fail( reject ).done( function( response ) { + var iframe; + + if ( '1' !== response ) { + reject(); + } + + iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide(); + iframe.appendTo( self.container ); + iframe.on( 'load', function() { + self.triedLogin = true; + + iframe.remove(); + self.run( deferred ); + }); + }); + }, + + destroy: function() { + api.Messenger.prototype.destroy.call( this ); + + if ( this.iframe ) { + this.iframe.remove(); + } + + delete this.iframe; + delete this.targetWindow; + } + }); + + (function(){ + var id = 0; + /** + * Return an incremented ID for a preview messenger channel. + * + * This function is named "uuid" for historical reasons, but it is a + * misnomer as it is not an actual UUID, and it is not universally unique. + * This is not to be confused with `api.settings.changeset.uuid`. + * + * @return {string} + */ + api.PreviewFrame.uuid = function() { + return 'preview-' + String( id++ ); + }; + }()); + + /** + * Set the document title of the customizer. + * + * @alias wp.customize.setDocumentTitle + * + * @since 4.1.0 + * + * @param {string} documentTitle + */ + api.setDocumentTitle = function ( documentTitle ) { + var tmpl, title; + tmpl = api.settings.documentTitleTmpl; + title = tmpl.replace( '%s', documentTitle ); + document.title = title; + api.trigger( 'title', title ); + }; + + api.Previewer = api.Messenger.extend(/** @lends wp.customize.Previewer.prototype */{ + refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh. + + /** + * @constructs wp.customize.Previewer + * @augments wp.customize.Messenger + * + * @param {Array} params.allowedUrls + * @param {string} params.container A selector or jQuery element for the preview + * frame to be placed. + * @param {string} params.form + * @param {string} params.previewUrl The URL to preview. + * @param {Object} options + */ + initialize: function( params, options ) { + var previewer = this, + urlParser = document.createElement( 'a' ); + + $.extend( previewer, options || {} ); + previewer.deferred = { + active: $.Deferred() + }; + + // Debounce to prevent hammering server and then wait for any pending update requests. + previewer.refresh = _.debounce( + ( function( originalRefresh ) { + return function() { + var isProcessingComplete, refreshOnceProcessingComplete; + isProcessingComplete = function() { + return 0 === api.state( 'processing' ).get(); + }; + if ( isProcessingComplete() ) { + originalRefresh.call( previewer ); + } else { + refreshOnceProcessingComplete = function() { + if ( isProcessingComplete() ) { + originalRefresh.call( previewer ); + api.state( 'processing' ).unbind( refreshOnceProcessingComplete ); + } + }; + api.state( 'processing' ).bind( refreshOnceProcessingComplete ); + } + }; + }( previewer.refresh ) ), + previewer.refreshBuffer + ); + + previewer.container = api.ensure( params.container ); + previewer.allowedUrls = params.allowedUrls; + + params.url = window.location.href; + + api.Messenger.prototype.initialize.call( previewer, params ); + + urlParser.href = previewer.origin(); + previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) ); + + /* + * Limit the URL to internal, front-end links. + * + * If the front end and the admin are served from the same domain, load the + * preview over ssl if the Customizer is being loaded over ssl. This avoids + * insecure content warnings. This is not attempted if the admin and front end + * are on different domains to avoid the case where the front end doesn't have + * ssl certs. + */ + + previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) { + var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = []; + urlParser = document.createElement( 'a' ); + urlParser.href = to; + + // Abort if URL is for admin or (static) files in wp-includes or wp-content. + if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) { + return null; + } + + // Remove state query params. + if ( urlParser.search.length > 1 ) { + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + delete queryParams.customize_changeset_uuid; + delete queryParams.customize_theme; + delete queryParams.customize_messenger_channel; + delete queryParams.customize_autosaved; + if ( _.isEmpty( queryParams ) ) { + urlParser.search = ''; + } else { + urlParser.search = $.param( queryParams ); + } + } + + parsedCandidateUrls.push( urlParser ); + + // Prepend list with URL that matches the scheme/protocol of the iframe. + if ( previewer.scheme.get() + ':' !== urlParser.protocol ) { + urlParser = document.createElement( 'a' ); + urlParser.href = parsedCandidateUrls[0].href; + urlParser.protocol = previewer.scheme.get() + ':'; + parsedCandidateUrls.unshift( urlParser ); + } + + // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL. + parsedAllowedUrl = document.createElement( 'a' ); + _.find( parsedCandidateUrls, function( parsedCandidateUrl ) { + return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) { + parsedAllowedUrl.href = allowedUrl; + if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) { + result = parsedCandidateUrl.href; + return true; + } + } ) ); + } ); + + return result; + }); + + previewer.bind( 'ready', previewer.ready ); + + // Start listening for keep-alive messages when iframe first loads. + previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) ); + + previewer.bind( 'synced', function() { + previewer.send( 'active' ); + } ); + + // Refresh the preview when the URL is changed (but not yet). + previewer.previewUrl.bind( previewer.refresh ); + + previewer.scroll = 0; + previewer.bind( 'scroll', function( distance ) { + previewer.scroll = distance; + }); + + // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh. + previewer.bind( 'url', function( url ) { + var onUrlChange, urlChanged = false; + previewer.scroll = 0; + onUrlChange = function() { + urlChanged = true; + }; + previewer.previewUrl.bind( onUrlChange ); + previewer.previewUrl.set( url ); + previewer.previewUrl.unbind( onUrlChange ); + if ( ! urlChanged ) { + previewer.refresh(); + } + } ); + + // Update the document title when the preview changes. + previewer.bind( 'documentTitle', function ( title ) { + api.setDocumentTitle( title ); + } ); + }, + + /** + * Handle the preview receiving the ready message. + * + * @since 4.7.0 + * @access public + * + * @param {Object} data - Data from preview. + * @param {string} data.currentUrl - Current URL. + * @param {Object} data.activePanels - Active panels. + * @param {Object} data.activeSections Active sections. + * @param {Object} data.activeControls Active controls. + * @return {void} + */ + ready: function( data ) { + var previewer = this, synced = {}, constructs; + + synced.settings = api.get(); + synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading; + if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) { + synced.scroll = previewer.scroll; + } + synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get(); + previewer.send( 'sync', synced ); + + // Set the previewUrl without causing the url to set the iframe. + if ( data.currentUrl ) { + previewer.previewUrl.unbind( previewer.refresh ); + previewer.previewUrl.set( data.currentUrl ); + previewer.previewUrl.bind( previewer.refresh ); + } + + /* + * Walk over all panels, sections, and controls and set their + * respective active states to true if the preview explicitly + * indicates as such. + */ + constructs = { + panel: data.activePanels, + section: data.activeSections, + control: data.activeControls + }; + _( constructs ).each( function ( activeConstructs, type ) { + api[ type ].each( function ( construct, id ) { + var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] ); + + /* + * If the construct was created statically in PHP (not dynamically in JS) + * then consider a missing (undefined) value in the activeConstructs to + * mean it should be deactivated (since it is gone). But if it is + * dynamically created then only toggle activation if the value is defined, + * as this means that the construct was also then correspondingly + * created statically in PHP and the active callback is available. + * Otherwise, dynamically-created constructs should normally have + * their active states toggled in JS rather than from PHP. + */ + if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) { + if ( activeConstructs[ id ] ) { + construct.activate(); + } else { + construct.deactivate(); + } + } + } ); + } ); + + if ( data.settingValidities ) { + api._handleSettingValidities( { + settingValidities: data.settingValidities, + focusInvalidControl: false + } ); + } + }, + + /** + * Keep the preview alive by listening for ready and keep-alive messages. + * + * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL. + * + * @since 4.7.0 + * @access public + * + * @return {void} + */ + keepPreviewAlive: function keepPreviewAlive() { + var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck; + + /** + * Schedule a preview keep-alive check. + * + * Note that if a page load takes longer than keepAliveCheck milliseconds, + * the keep-alive messages will still be getting sent from the previous + * URL. + */ + scheduleKeepAliveCheck = function() { + timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck ); + }; + + /** + * Set the previewerAlive state to true when receiving a message from the preview. + */ + keepAliveTick = function() { + api.state( 'previewerAlive' ).set( true ); + clearTimeout( timeoutId ); + scheduleKeepAliveCheck(); + }; + + /** + * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message. + * + * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser + * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage + * transport to use refresh instead, causing the preview frame also to be replaced with the current + * allowed preview URL. + */ + handleMissingKeepAlive = function() { + api.state( 'previewerAlive' ).set( false ); + }; + scheduleKeepAliveCheck(); + + previewer.bind( 'ready', keepAliveTick ); + previewer.bind( 'keep-alive', keepAliveTick ); + }, + + /** + * Query string data sent with each preview request. + * + * @abstract + */ + query: function() {}, + + abort: function() { + if ( this.loading ) { + this.loading.destroy(); + delete this.loading; + } + }, + + /** + * Refresh the preview seamlessly. + * + * @since 3.4.0 + * @access public + * + * @return {void} + */ + refresh: function() { + var previewer = this, onSettingChange; + + // Display loading indicator. + previewer.send( 'loading-initiated' ); + + previewer.abort(); + + previewer.loading = new api.PreviewFrame({ + url: previewer.url(), + previewUrl: previewer.previewUrl(), + query: previewer.query( { excludeCustomizedSaved: true } ) || {}, + container: previewer.container + }); + + previewer.settingsModifiedWhileLoading = {}; + onSettingChange = function( setting ) { + previewer.settingsModifiedWhileLoading[ setting.id ] = true; + }; + api.bind( 'change', onSettingChange ); + previewer.loading.always( function() { + api.unbind( 'change', onSettingChange ); + } ); + + previewer.loading.done( function( readyData ) { + var loadingFrame = this, onceSynced; + + previewer.preview = loadingFrame; + previewer.targetWindow( loadingFrame.targetWindow() ); + previewer.channel( loadingFrame.channel() ); + + onceSynced = function() { + loadingFrame.unbind( 'synced', onceSynced ); + if ( previewer._previousPreview ) { + previewer._previousPreview.destroy(); + } + previewer._previousPreview = previewer.preview; + previewer.deferred.active.resolve(); + delete previewer.loading; + }; + loadingFrame.bind( 'synced', onceSynced ); + + // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh. + previewer.trigger( 'ready', readyData ); + }); + + previewer.loading.fail( function( reason ) { + previewer.send( 'loading-failed' ); + + if ( 'logged out' === reason ) { + if ( previewer.preview ) { + previewer.preview.destroy(); + delete previewer.preview; + } + + previewer.login().done( previewer.refresh ); + } + + if ( 'cheatin' === reason ) { + previewer.cheatin(); + } + }); + }, + + login: function() { + var previewer = this, + deferred, messenger, iframe; + + if ( this._login ) { + return this._login; + } + + deferred = $.Deferred(); + this._login = deferred.promise(); + + messenger = new api.Messenger({ + channel: 'login', + url: api.settings.url.login + }); + + iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container ); + + messenger.targetWindow( iframe[0].contentWindow ); + + messenger.bind( 'login', function () { + var refreshNonces = previewer.refreshNonces(); + + refreshNonces.always( function() { + iframe.remove(); + messenger.destroy(); + delete previewer._login; + }); + + refreshNonces.done( function() { + deferred.resolve(); + }); + + refreshNonces.fail( function() { + previewer.cheatin(); + deferred.reject(); + }); + }); + + return this._login; + }, + + cheatin: function() { + $( document.body ).empty().addClass( 'cheatin' ).append( + '<h1>' + api.l10n.notAllowedHeading + '</h1>' + + '<p>' + api.l10n.notAllowed + '</p>' + ); + }, + + refreshNonces: function() { + var request, deferred = $.Deferred(); + + deferred.promise(); + + request = wp.ajax.post( 'customize_refresh_nonces', { + wp_customize: 'on', + customize_theme: api.settings.theme.stylesheet + }); + + request.done( function( response ) { + api.trigger( 'nonce-refresh', response ); + deferred.resolve(); + }); + + request.fail( function() { + deferred.reject(); + }); + + return deferred; + } + }); + + api.settingConstructor = {}; + api.controlConstructor = { + color: api.ColorControl, + media: api.MediaControl, + upload: api.UploadControl, + image: api.ImageControl, + cropped_image: api.CroppedImageControl, + site_icon: api.SiteIconControl, + header: api.HeaderControl, + background: api.BackgroundControl, + background_position: api.BackgroundPositionControl, + theme: api.ThemeControl, + date_time: api.DateTimeControl, + code_editor: api.CodeEditorControl + }; + api.panelConstructor = { + themes: api.ThemesPanel + }; + api.sectionConstructor = { + themes: api.ThemesSection, + outer: api.OuterSection + }; + + /** + * Handle setting_validities in an error response for the customize-save request. + * + * Add notifications to the settings and focus on the first control that has an invalid setting. + * + * @alias wp.customize._handleSettingValidities + * + * @since 4.6.0 + * @private + * + * @param {Object} args + * @param {Object} args.settingValidities + * @param {boolean} [args.focusInvalidControl=false] + * @return {void} + */ + api._handleSettingValidities = function handleSettingValidities( args ) { + var invalidSettingControls, invalidSettings = [], wasFocused = false; + + // Find the controls that correspond to each invalid setting. + _.each( args.settingValidities, function( validity, settingId ) { + var setting = api( settingId ); + if ( setting ) { + + // Add notifications for invalidities. + if ( _.isObject( validity ) ) { + _.each( validity, function( params, code ) { + var notification, existingNotification, needsReplacement = false; + notification = new api.Notification( code, _.extend( { fromServer: true }, params ) ); + + // Remove existing notification if already exists for code but differs in parameters. + existingNotification = setting.notifications( notification.code ); + if ( existingNotification ) { + needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data ); + } + if ( needsReplacement ) { + setting.notifications.remove( code ); + } + + if ( ! setting.notifications.has( notification.code ) ) { + setting.notifications.add( notification ); + } + invalidSettings.push( setting.id ); + } ); + } + + // Remove notification errors that are no longer valid. + setting.notifications.each( function( notification ) { + if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) { + setting.notifications.remove( notification.code ); + } + } ); + } + } ); + + if ( args.focusInvalidControl ) { + invalidSettingControls = api.findControlsForSettings( invalidSettings ); + + // Focus on the first control that is inside of an expanded section (one that is visible). + _( _.values( invalidSettingControls ) ).find( function( controls ) { + return _( controls ).find( function( control ) { + var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded(); + if ( isExpanded && control.expanded ) { + isExpanded = control.expanded(); + } + if ( isExpanded ) { + control.focus(); + wasFocused = true; + } + return wasFocused; + } ); + } ); + + // Focus on the first invalid control. + if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) { + _.values( invalidSettingControls )[0][0].focus(); + } + } + }; + + /** + * Find all controls associated with the given settings. + * + * @alias wp.customize.findControlsForSettings + * + * @since 4.6.0 + * @param {string[]} settingIds Setting IDs. + * @return {Object<string, wp.customize.Control>} Mapping setting ids to arrays of controls. + */ + api.findControlsForSettings = function findControlsForSettings( settingIds ) { + var controls = {}, settingControls; + _.each( _.unique( settingIds ), function( settingId ) { + var setting = api( settingId ); + if ( setting ) { + settingControls = setting.findControls(); + if ( settingControls && settingControls.length > 0 ) { + controls[ settingId ] = settingControls; + } + } + } ); + return controls; + }; + + /** + * Sort panels, sections, controls by priorities. Hide empty sections and panels. + * + * @alias wp.customize.reflowPaneContents + * + * @since 4.1.0 + */ + api.reflowPaneContents = _.bind( function () { + + var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false; + + if ( document.activeElement ) { + activeElement = $( document.activeElement ); + } + + // Sort the sections within each panel. + api.panel.each( function ( panel ) { + if ( 'themes' === panel.id ) { + return; // Don't reflow theme sections, as doing so moves them after the themes container. + } + + var sections = panel.sections(), + sectionHeadContainers = _.pluck( sections, 'headContainer' ); + rootNodes.push( panel ); + appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' ); + if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) { + _( sections ).each( function ( section ) { + appendContainer.append( section.headContainer ); + } ); + wasReflowed = true; + } + } ); + + // Sort the controls within each section. + api.section.each( function ( section ) { + var controls = section.controls(), + controlContainers = _.pluck( controls, 'container' ); + if ( ! section.panel() ) { + rootNodes.push( section ); + } + appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' ); + if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) { + _( controls ).each( function ( control ) { + appendContainer.append( control.container ); + } ); + wasReflowed = true; + } + } ); + + // Sort the root panels and sections. + rootNodes.sort( api.utils.prioritySort ); + rootHeadContainers = _.pluck( rootNodes, 'headContainer' ); + appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable. + if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) { + _( rootNodes ).each( function ( rootNode ) { + appendContainer.append( rootNode.headContainer ); + } ); + wasReflowed = true; + } + + // Now re-trigger the active Value callbacks so that the panels and sections can decide whether they can be rendered. + api.panel.each( function ( panel ) { + var value = panel.active(); + panel.active.callbacks.fireWith( panel.active, [ value, value ] ); + } ); + api.section.each( function ( section ) { + var value = section.active(); + section.active.callbacks.fireWith( section.active, [ value, value ] ); + } ); + + // Restore focus if there was a reflow and there was an active (focused) element. + if ( wasReflowed && activeElement ) { + activeElement.trigger( 'focus' ); + } + api.trigger( 'pane-contents-reflowed' ); + }, api ); + + // Define state values. + api.state = new api.Values(); + _.each( [ + 'saved', + 'saving', + 'trashing', + 'activated', + 'processing', + 'paneVisible', + 'expandedPanel', + 'expandedSection', + 'changesetDate', + 'selectedChangesetDate', + 'changesetStatus', + 'selectedChangesetStatus', + 'remainingTimeToPublish', + 'previewerAlive', + 'editShortcutVisibility', + 'changesetLocked', + 'previewedDevice' + ], function( name ) { + api.state.create( name ); + }); + + $( function() { + api.settings = window._wpCustomizeSettings; + api.l10n = window._wpCustomizeControlsL10n; + + // Check if we can run the Customizer. + if ( ! api.settings ) { + return; + } + + // Bail if any incompatibilities are found. + if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) { + return; + } + + if ( null === api.PreviewFrame.prototype.sensitivity ) { + api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity; + } + if ( null === api.Previewer.prototype.refreshBuffer ) { + api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh; + } + + var parent, + body = $( document.body ), + overlay = body.children( '.wp-full-overlay' ), + title = $( '#customize-info .panel-title.site-title' ), + closeBtn = $( '.customize-controls-close' ), + saveBtn = $( '#save' ), + btnWrapper = $( '#customize-save-button-wrapper' ), + publishSettingsBtn = $( '#publish-settings' ), + footerActions = $( '#customize-footer-actions' ); + + // Add publish settings section in JS instead of PHP since the Customizer depends on it to function. + api.bind( 'ready', function() { + api.section.add( new api.OuterSection( 'publish_settings', { + title: api.l10n.publishSettings, + priority: 0, + active: api.settings.theme.active + } ) ); + } ); + + // Set up publish settings section and its controls. + api.section( 'publish_settings', function( section ) { + var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000; + + trashControl = new api.Control( 'trash_changeset', { + type: 'button', + section: section.id, + priority: 30, + input_attrs: { + 'class': 'button-link button-link-delete', + value: api.l10n.discardChanges + } + } ); + api.control.add( trashControl ); + trashControl.deferred.embedded.done( function() { + trashControl.container.find( '.button-link' ).on( 'click', function() { + if ( confirm( api.l10n.trashConfirm ) ) { + wp.customize.previewer.trash(); + } + } ); + } ); + + api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', { + section: section.id, + priority: 100 + } ) ); + + /** + * Return whether the pubish settings section should be active. + * + * @return {boolean} Is section active. + */ + isSectionActive = function() { + if ( ! api.state( 'activated' ).get() ) { + return false; + } + if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) { + return false; + } + if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) { + return false; + } + return true; + }; + + // Make sure publish settings are not available while the theme is not active and the customizer is in a published state. + section.active.validate = isSectionActive; + updateSectionActive = function() { + section.active.set( isSectionActive() ); + }; + api.state( 'activated' ).bind( updateSectionActive ); + api.state( 'trashing' ).bind( updateSectionActive ); + api.state( 'saved' ).bind( updateSectionActive ); + api.state( 'changesetStatus' ).bind( updateSectionActive ); + updateSectionActive(); + + // Bind visibility of the publish settings button to whether the section is active. + updateButtonsState = function() { + publishSettingsBtn.toggle( section.active.get() ); + saveBtn.toggleClass( 'has-next-sibling', section.active.get() ); + }; + updateButtonsState(); + section.active.bind( updateButtonsState ); + + function highlightScheduleButton() { + if ( ! cancelScheduleButtonReminder ) { + cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, { + delay: 1000, + + /* + * Only abort the reminder when the save button is focused. + * If the user clicks the settings button to toggle the + * settings closed, we'll still remind them. + */ + focusTarget: saveBtn + } ); + } + } + function cancelHighlightScheduleButton() { + if ( cancelScheduleButtonReminder ) { + cancelScheduleButtonReminder(); + cancelScheduleButtonReminder = null; + } + } + api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton ); + + section.contentContainer.find( '.customize-action' ).text( api.l10n.updating ); + section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' ); + publishSettingsBtn.prop( 'disabled', false ); + + publishSettingsBtn.on( 'click', function( event ) { + event.preventDefault(); + section.expanded.set( ! section.expanded.get() ); + } ); + + section.expanded.bind( function( isExpanded ) { + var defaultChangesetStatus; + publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) ); + publishSettingsBtn.toggleClass( 'active', isExpanded ); + + if ( isExpanded ) { + cancelHighlightScheduleButton(); + return; + } + + defaultChangesetStatus = api.state( 'changesetStatus' ).get(); + if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) { + defaultChangesetStatus = 'publish'; + } + + if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) { + highlightScheduleButton(); + } else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) { + highlightScheduleButton(); + } + } ); + + statusControl = new api.Control( 'changeset_status', { + priority: 10, + type: 'radio', + section: 'publish_settings', + setting: api.state( 'selectedChangesetStatus' ), + templateId: 'customize-selected-changeset-status-control', + label: api.l10n.action, + choices: api.settings.changeset.statusChoices + } ); + api.control.add( statusControl ); + + dateControl = new api.DateTimeControl( 'changeset_scheduled_date', { + priority: 20, + section: 'publish_settings', + setting: api.state( 'selectedChangesetDate' ), + minYear: ( new Date() ).getFullYear(), + allowPastDate: false, + includeTime: true, + twelveHourFormat: /a/i.test( api.settings.timeFormat ), + description: api.l10n.scheduleDescription + } ); + dateControl.notifications.alt = true; + api.control.add( dateControl ); + + publishWhenTime = function() { + api.state( 'selectedChangesetStatus' ).set( 'publish' ); + api.previewer.save(); + }; + + // Start countdown for when the dateTime arrives, or clear interval when it is . + updateTimeArrivedPoller = function() { + var shouldPoll = ( + 'future' === api.state( 'changesetStatus' ).get() && + 'future' === api.state( 'selectedChangesetStatus' ).get() && + api.state( 'changesetDate' ).get() && + api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() && + api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0 + ); + + if ( shouldPoll && ! pollInterval ) { + pollInterval = setInterval( function() { + var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ); + api.state( 'remainingTimeToPublish' ).set( remainingTime ); + if ( remainingTime <= 0 ) { + clearInterval( pollInterval ); + pollInterval = 0; + publishWhenTime(); + } + }, timeArrivedPollingInterval ); + } else if ( ! shouldPoll && pollInterval ) { + clearInterval( pollInterval ); + pollInterval = 0; + } + }; + + api.state( 'changesetDate' ).bind( updateTimeArrivedPoller ); + api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller ); + api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller ); + api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller ); + updateTimeArrivedPoller(); + + // Ensure dateControl only appears when selected status is future. + dateControl.active.validate = function() { + return 'future' === api.state( 'selectedChangesetStatus' ).get(); + }; + toggleDateControl = function( value ) { + dateControl.active.set( 'future' === value ); + }; + toggleDateControl( api.state( 'selectedChangesetStatus' ).get() ); + api.state( 'selectedChangesetStatus' ).bind( toggleDateControl ); + + // Show notification on date control when status is future but it isn't a future date. + api.state( 'saving' ).bind( function( isSaving ) { + if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) { + dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() ); + } + } ); + } ); + + // Prevent the form from saving when enter is pressed on an input or select element. + $('#customize-controls').on( 'keydown', function( e ) { + var isEnter = ( 13 === e.which ), + $el = $( e.target ); + + if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) { + e.preventDefault(); + } + }); + + // Expand/Collapse the main customizer customize info. + $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() { + var section = $( this ).closest( '.accordion-section' ), + content = section.find( '.customize-panel-description:first' ); + + if ( section.hasClass( 'cannot-expand' ) ) { + return; + } + + if ( section.hasClass( 'open' ) ) { + section.toggleClass( 'open' ); + content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() { + content.trigger( 'toggled' ); + } ); + $( this ).attr( 'aria-expanded', false ); + } else { + content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() { + content.trigger( 'toggled' ); + } ); + section.toggleClass( 'open' ); + $( this ).attr( 'aria-expanded', true ); + } + }); + + /** + * Initialize Previewer + * + * @alias wp.customize.previewer + */ + api.previewer = new api.Previewer({ + container: '#customize-preview', + form: '#customize-controls', + previewUrl: api.settings.url.preview, + allowedUrls: api.settings.url.allowed + },/** @lends wp.customize.previewer */{ + + nonce: api.settings.nonce, + + /** + * Build the query to send along with the Preview request. + * + * @since 3.4.0 + * @since 4.7.0 Added options param. + * @access public + * + * @param {Object} [options] Options. + * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset). + * @return {Object} Query vars. + */ + query: function( options ) { + var queryVars = { + wp_customize: 'on', + customize_theme: api.settings.theme.stylesheet, + nonce: this.nonce.preview, + customize_changeset_uuid: api.settings.changeset.uuid + }; + if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) { + queryVars.customize_autosaved = 'on'; + } + + /* + * Exclude customized data if requested especially for calls to requestChangesetUpdate. + * Changeset updates are differential and so it is a performance waste to send all of + * the dirty settings with each update. + */ + queryVars.customized = JSON.stringify( api.dirtyValues( { + unsaved: options && options.excludeCustomizedSaved + } ) ); + + return queryVars; + }, + + /** + * Save (and publish) the customizer changeset. + * + * Updates to the changeset are transactional. If any of the settings + * are invalid then none of them will be written into the changeset. + * A revision will be made for the changeset post if revisions support + * has been added to the post type. + * + * @since 3.4.0 + * @since 4.7.0 Added args param and return value. + * + * @param {Object} [args] Args. + * @param {string} [args.status=publish] Status. + * @param {string} [args.date] Date, in local time in MySQL format. + * @param {string} [args.title] Title + * @return {jQuery.promise} Promise. + */ + save: function( args ) { + var previewer = this, + deferred = $.Deferred(), + changesetStatus = api.state( 'selectedChangesetStatus' ).get(), + selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(), + processing = api.state( 'processing' ), + submitWhenDoneProcessing, + submit, + modifiedWhileSaving = {}, + invalidSettings = [], + invalidControls = [], + invalidSettingLessControls = []; + + if ( args && args.status ) { + changesetStatus = args.status; + } + + if ( api.state( 'saving' ).get() ) { + deferred.reject( 'already_saving' ); + deferred.promise(); + } + + api.state( 'saving' ).set( true ); + + function captureSettingModifiedDuringSave( setting ) { + modifiedWhileSaving[ setting.id ] = true; + } + + submit = function () { + var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error'; + + api.bind( 'change', captureSettingModifiedDuringSave ); + api.notifications.remove( errorCode ); + + /* + * Block saving if there are any settings that are marked as + * invalid from the client (not from the server). Focus on + * the control. + */ + api.each( function( setting ) { + setting.notifications.each( function( notification ) { + if ( 'error' === notification.type && ! notification.fromServer ) { + invalidSettings.push( setting.id ); + if ( ! settingInvalidities[ setting.id ] ) { + settingInvalidities[ setting.id ] = {}; + } + settingInvalidities[ setting.id ][ notification.code ] = notification; + } + } ); + } ); + + // Find all invalid setting less controls with notification type error. + api.control.each( function( control ) { + if ( ! control.setting || ! control.setting.id && control.active.get() ) { + control.notifications.each( function( notification ) { + if ( 'error' === notification.type ) { + invalidSettingLessControls.push( [ control ] ); + } + } ); + } + } ); + + invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) ); + if ( ! _.isEmpty( invalidControls ) ) { + + invalidControls[0][0].focus(); + api.unbind( 'change', captureSettingModifiedDuringSave ); + + if ( invalidSettings.length ) { + api.notifications.add( new api.Notification( errorCode, { + message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ), + type: 'error', + dismissible: true, + saveFailure: true + } ) ); + } + + deferred.rejectWith( previewer, [ + { setting_invalidities: settingInvalidities } + ] ); + api.state( 'saving' ).set( false ); + return deferred.promise(); + } + + /* + * Note that excludeCustomizedSaved is intentionally false so that the entire + * set of customized data will be included if bypassed changeset update. + */ + query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), { + nonce: previewer.nonce.save, + customize_changeset_status: changesetStatus + } ); + + if ( args && args.date ) { + query.customize_changeset_date = args.date; + } else if ( 'future' === changesetStatus && selectedChangesetDate ) { + query.customize_changeset_date = selectedChangesetDate; + } + + if ( args && args.title ) { + query.customize_changeset_title = args.title; + } + + // Allow plugins to modify the params included with the save request. + api.trigger( 'save-request-params', query ); + + /* + * Note that the dirty customized values will have already been set in the + * changeset and so technically query.customized could be deleted. However, + * it is remaining here to make sure that any settings that got updated + * quietly which may have not triggered an update request will also get + * included in the values that get saved to the changeset. This will ensure + * that values that get injected via the saved event will be included in + * the changeset. This also ensures that setting values that were invalid + * will get re-validated, perhaps in the case of settings that are invalid + * due to dependencies on other settings. + */ + request = wp.ajax.post( 'customize_save', query ); + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); + + api.trigger( 'save', request ); + + request.always( function () { + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); + api.state( 'saving' ).set( false ); + api.unbind( 'change', captureSettingModifiedDuringSave ); + } ); + + // Remove notifications that were added due to save failures. + api.notifications.each( function( notification ) { + if ( notification.saveFailure ) { + api.notifications.remove( notification.code ); + } + }); + + request.fail( function ( response ) { + var notification, notificationArgs; + notificationArgs = { + type: 'error', + dismissible: true, + fromServer: true, + saveFailure: true + }; + + if ( '0' === response ) { + response = 'not_logged_in'; + } else if ( '-1' === response ) { + // Back-compat in case any other check_ajax_referer() call is dying. + response = 'invalid_nonce'; + } + + if ( 'invalid_nonce' === response ) { + previewer.cheatin(); + } else if ( 'not_logged_in' === response ) { + previewer.preview.iframe.hide(); + previewer.login().done( function() { + previewer.save(); + previewer.preview.iframe.show(); + } ); + } else if ( response.code ) { + if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) { + api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus(); + } else if ( 'changeset_locked' !== response.code ) { + notification = new api.Notification( response.code, _.extend( notificationArgs, { + message: response.message + } ) ); + } + } else { + notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, { + message: api.l10n.unknownRequestFail + } ) ); + } + + if ( notification ) { + api.notifications.add( notification ); + } + + if ( response.setting_validities ) { + api._handleSettingValidities( { + settingValidities: response.setting_validities, + focusInvalidControl: true + } ); + } + + deferred.rejectWith( previewer, [ response ] ); + api.trigger( 'error', response ); + + // Start a new changeset if the underlying changeset was published. + if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) { + api.settings.changeset.uuid = response.next_changeset_uuid; + api.state( 'changesetStatus' ).set( '' ); + if ( api.settings.changeset.branching ) { + parent.send( 'changeset-uuid', api.settings.changeset.uuid ); + } + api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid ); + } + } ); + + request.done( function( response ) { + + previewer.send( 'saved', response ); + + api.state( 'changesetStatus' ).set( response.changeset_status ); + if ( response.changeset_date ) { + api.state( 'changesetDate' ).set( response.changeset_date ); + } + + if ( 'publish' === response.changeset_status ) { + + // Mark all published as clean if they haven't been modified during the request. + api.each( function( setting ) { + /* + * Note that the setting revision will be undefined in the case of setting + * values that are marked as dirty when the customizer is loaded, such as + * when applying starter content. All other dirty settings will have an + * associated revision due to their modification triggering a change event. + */ + if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) { + setting._dirty = false; + } + } ); + + api.state( 'changesetStatus' ).set( '' ); + api.settings.changeset.uuid = response.next_changeset_uuid; + if ( api.settings.changeset.branching ) { + parent.send( 'changeset-uuid', api.settings.changeset.uuid ); + } + } + + // Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved. + api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision ); + + if ( response.setting_validities ) { + api._handleSettingValidities( { + settingValidities: response.setting_validities, + focusInvalidControl: true + } ); + } + + deferred.resolveWith( previewer, [ response ] ); + api.trigger( 'saved', response ); + + // Restore the global dirty state if any settings were modified during save. + if ( ! _.isEmpty( modifiedWhileSaving ) ) { + api.state( 'saved' ).set( false ); + } + } ); + }; + + if ( 0 === processing() ) { + submit(); + } else { + submitWhenDoneProcessing = function () { + if ( 0 === processing() ) { + api.state.unbind( 'change', submitWhenDoneProcessing ); + submit(); + } + }; + api.state.bind( 'change', submitWhenDoneProcessing ); + } + + return deferred.promise(); + }, + + /** + * Trash the current changes. + * + * Revert the Customizer to its previously-published state. + * + * @since 4.9.0 + * + * @return {jQuery.promise} Promise. + */ + trash: function trash() { + var request, success, fail; + + api.state( 'trashing' ).set( true ); + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); + + request = wp.ajax.post( 'customize_trash', { + customize_changeset_uuid: api.settings.changeset.uuid, + nonce: api.settings.nonce.trash + } ); + api.notifications.add( new api.OverlayNotification( 'changeset_trashing', { + type: 'info', + message: api.l10n.revertingChanges, + loading: true + } ) ); + + success = function() { + var urlParser = document.createElement( 'a' ), queryParams; + + api.state( 'changesetStatus' ).set( 'trash' ); + api.each( function( setting ) { + setting._dirty = false; + } ); + api.state( 'saved' ).set( true ); + + // Go back to Customizer without changeset. + urlParser.href = location.href; + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + delete queryParams.changeset_uuid; + queryParams['return'] = api.settings.url['return']; + urlParser.search = $.param( queryParams ); + location.replace( urlParser.href ); + }; + + fail = function( code, message ) { + var notificationCode = code || 'unknown_error'; + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); + api.state( 'trashing' ).set( false ); + api.notifications.remove( 'changeset_trashing' ); + api.notifications.add( new api.Notification( notificationCode, { + message: message || api.l10n.unknownError, + dismissible: true, + type: 'error' + } ) ); + }; + + request.done( function( response ) { + success( response.message ); + } ); + + request.fail( function( response ) { + var code = response.code || 'trashing_failed'; + if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) { + success( response.message ); + } else { + fail( code, response.message ); + } + } ); + }, + + /** + * Builds the front preview URL with the current state of customizer. + * + * @since 4.9.0 + * + * @return {string} Preview URL. + */ + getFrontendPreviewUrl: function() { + var previewer = this, params, urlParser; + urlParser = document.createElement( 'a' ); + urlParser.href = previewer.previewUrl.get(); + params = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + + if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) { + params.customize_changeset_uuid = api.settings.changeset.uuid; + } + if ( ! api.state( 'activated' ).get() ) { + params.customize_theme = api.settings.theme.stylesheet; + } + + urlParser.search = $.param( params ); + return urlParser.href; + } + }); + + // Ensure preview nonce is included with every customized request, to allow post data to be read. + $.ajaxPrefilter( function injectPreviewNonce( options ) { + if ( ! /wp_customize=on/.test( options.data ) ) { + return; + } + options.data += '&' + $.param({ + customize_preview_nonce: api.settings.nonce.preview + }); + }); + + // Refresh the nonces if the preview sends updated nonces over. + api.previewer.bind( 'nonce', function( nonce ) { + $.extend( this.nonce, nonce ); + }); + + // Refresh the nonces if login sends updated nonces over. + api.bind( 'nonce-refresh', function( nonce ) { + $.extend( api.settings.nonce, nonce ); + $.extend( api.previewer.nonce, nonce ); + api.previewer.send( 'nonce-refresh', nonce ); + }); + + // Create Settings. + $.each( api.settings.settings, function( id, data ) { + var Constructor = api.settingConstructor[ data.type ] || api.Setting; + api.add( new Constructor( id, data.value, { + transport: data.transport, + previewer: api.previewer, + dirty: !! data.dirty + } ) ); + }); + + // Create Panels. + $.each( api.settings.panels, function ( id, data ) { + var Constructor = api.panelConstructor[ data.type ] || api.Panel, options; + // Inclusion of params alias is for back-compat for custom panels that expect to augment this property. + options = _.extend( { params: data }, data ); + api.panel.add( new Constructor( id, options ) ); + }); + + // Create Sections. + $.each( api.settings.sections, function ( id, data ) { + var Constructor = api.sectionConstructor[ data.type ] || api.Section, options; + // Inclusion of params alias is for back-compat for custom sections that expect to augment this property. + options = _.extend( { params: data }, data ); + api.section.add( new Constructor( id, options ) ); + }); + + // Create Controls. + $.each( api.settings.controls, function( id, data ) { + var Constructor = api.controlConstructor[ data.type ] || api.Control, options; + // Inclusion of params alias is for back-compat for custom controls that expect to augment this property. + options = _.extend( { params: data }, data ); + api.control.add( new Constructor( id, options ) ); + }); + + // Focus the autofocused element. + _.each( [ 'panel', 'section', 'control' ], function( type ) { + var id = api.settings.autofocus[ type ]; + if ( ! id ) { + return; + } + + /* + * Defer focus until: + * 1. The panel, section, or control exists (especially for dynamically-created ones). + * 2. The instance is embedded in the document (and so is focusable). + * 3. The preview has finished loading so that the active states have been set. + */ + api[ type ]( id, function( instance ) { + instance.deferred.embedded.done( function() { + api.previewer.deferred.active.done( function() { + instance.focus(); + }); + }); + }); + }); + + api.bind( 'ready', api.reflowPaneContents ); + $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { + var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents ); + values.bind( 'add', debouncedReflowPaneContents ); + values.bind( 'change', debouncedReflowPaneContents ); + values.bind( 'remove', debouncedReflowPaneContents ); + } ); + + // Set up global notifications area. + api.bind( 'ready', function setUpGlobalNotificationsArea() { + var sidebar, containerHeight, containerInitialTop; + api.notifications.container = $( '#customize-notifications-area' ); + + api.notifications.bind( 'change', _.debounce( function() { + api.notifications.render(); + } ) ); + + sidebar = $( '.wp-full-overlay-sidebar-content' ); + api.notifications.bind( 'rendered', function updateSidebarTop() { + sidebar.css( 'top', '' ); + if ( 0 !== api.notifications.count() ) { + containerHeight = api.notifications.container.outerHeight() + 1; + containerInitialTop = parseInt( sidebar.css( 'top' ), 10 ); + sidebar.css( 'top', containerInitialTop + containerHeight + 'px' ); + } + api.notifications.trigger( 'sidebarTopUpdated' ); + }); + + api.notifications.render(); + }); + + // Save and activated states. + (function( state ) { + var saved = state.instance( 'saved' ), + saving = state.instance( 'saving' ), + trashing = state.instance( 'trashing' ), + activated = state.instance( 'activated' ), + processing = state.instance( 'processing' ), + paneVisible = state.instance( 'paneVisible' ), + expandedPanel = state.instance( 'expandedPanel' ), + expandedSection = state.instance( 'expandedSection' ), + changesetStatus = state.instance( 'changesetStatus' ), + selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ), + changesetDate = state.instance( 'changesetDate' ), + selectedChangesetDate = state.instance( 'selectedChangesetDate' ), + previewerAlive = state.instance( 'previewerAlive' ), + editShortcutVisibility = state.instance( 'editShortcutVisibility' ), + changesetLocked = state.instance( 'changesetLocked' ), + populateChangesetUuidParam, defaultSelectedChangesetStatus; + + state.bind( 'change', function() { + var canSave; + + if ( ! activated() ) { + saveBtn.val( api.l10n.activate ); + closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); + + } else if ( '' === changesetStatus.get() && saved() ) { + if ( api.settings.changeset.currentUserCanPublish ) { + saveBtn.val( api.l10n.published ); + } else { + saveBtn.val( api.l10n.saved ); + } + closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); + + } else { + if ( 'draft' === selectedChangesetStatus() ) { + if ( saved() && selectedChangesetStatus() === changesetStatus() ) { + saveBtn.val( api.l10n.draftSaved ); + } else { + saveBtn.val( api.l10n.saveDraft ); + } + } else if ( 'future' === selectedChangesetStatus() ) { + if ( saved() && selectedChangesetStatus() === changesetStatus() ) { + if ( changesetDate.get() !== selectedChangesetDate.get() ) { + saveBtn.val( api.l10n.schedule ); + } else { + saveBtn.val( api.l10n.scheduled ); + } + } else { + saveBtn.val( api.l10n.schedule ); + } + } else if ( api.settings.changeset.currentUserCanPublish ) { + saveBtn.val( api.l10n.publish ); + } + closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); + } + + /* + * Save (publish) button should be enabled if saving is not currently happening, + * and if the theme is not active or the changeset exists but is not published. + */ + canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) ); + + saveBtn.prop( 'disabled', ! canSave ); + }); + + selectedChangesetStatus.validate = function( status ) { + if ( '' === status || 'auto-draft' === status ) { + return null; + } + return status; + }; + + defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft'; + + // Set default states. + changesetStatus( api.settings.changeset.status ); + changesetLocked( Boolean( api.settings.changeset.lockUser ) ); + changesetDate( api.settings.changeset.publishDate ); + selectedChangesetDate( api.settings.changeset.publishDate ); + selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status ); + selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection. + saved( true ); + if ( '' === changesetStatus() ) { // Handle case for loading starter content. + api.each( function( setting ) { + if ( setting._dirty ) { + saved( false ); + } + } ); + } + saving( false ); + activated( api.settings.theme.active ); + processing( 0 ); + paneVisible( true ); + expandedPanel( false ); + expandedSection( false ); + previewerAlive( true ); + editShortcutVisibility( 'visible' ); + + api.bind( 'change', function() { + if ( state( 'saved' ).get() ) { + state( 'saved' ).set( false ); + } + }); + + // Populate changeset UUID param when state becomes dirty. + if ( api.settings.changeset.branching ) { + saved.bind( function( isSaved ) { + if ( ! isSaved ) { + populateChangesetUuidParam( true ); + } + }); + } + + saving.bind( function( isSaving ) { + body.toggleClass( 'saving', isSaving ); + } ); + trashing.bind( function( isTrashing ) { + body.toggleClass( 'trashing', isTrashing ); + } ); + + api.bind( 'saved', function( response ) { + state('saved').set( true ); + if ( 'publish' === response.changeset_status ) { + state( 'activated' ).set( true ); + } + }); + + activated.bind( function( to ) { + if ( to ) { + api.trigger( 'activated' ); + } + }); + + /** + * Populate URL with UUID via `history.replaceState()`. + * + * @since 4.7.0 + * @access private + * + * @param {boolean} isIncluded Is UUID included. + * @return {void} + */ + populateChangesetUuidParam = function( isIncluded ) { + var urlParser, queryParams; + + // Abort on IE9 which doesn't support history management. + if ( ! history.replaceState ) { + return; + } + + urlParser = document.createElement( 'a' ); + urlParser.href = location.href; + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + if ( isIncluded ) { + if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) { + return; + } + queryParams.changeset_uuid = api.settings.changeset.uuid; + } else { + if ( ! queryParams.changeset_uuid ) { + return; + } + delete queryParams.changeset_uuid; + } + urlParser.search = $.param( queryParams ); + history.replaceState( {}, document.title, urlParser.href ); + }; + + // Show changeset UUID in URL when in branching mode and there is a saved changeset. + if ( api.settings.changeset.branching ) { + changesetStatus.bind( function( newStatus ) { + populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus ); + } ); + } + }( api.state ) ); + + /** + * Handles lock notice and take over request. + * + * @since 4.9.0 + */ + ( function checkAndDisplayLockNotice() { + + var LockedNotification = api.OverlayNotification.extend(/** @lends wp.customize~LockedNotification.prototype */{ + + /** + * Template ID. + * + * @type {string} + */ + templateId: 'customize-changeset-locked-notification', + + /** + * Lock user. + * + * @type {object} + */ + lockUser: null, + + /** + * A notification that is displayed in a full-screen overlay with information about the locked changeset. + * + * @constructs wp.customize~LockedNotification + * @augments wp.customize.OverlayNotification + * + * @since 4.9.0 + * + * @param {string} [code] - Code. + * @param {Object} [params] - Params. + */ + initialize: function( code, params ) { + var notification = this, _code, _params; + _code = code || 'changeset_locked'; + _params = _.extend( + { + message: '', + type: 'warning', + containerClasses: '', + lockUser: {} + }, + params + ); + _params.containerClasses += ' notification-changeset-locked'; + api.OverlayNotification.prototype.initialize.call( notification, _code, _params ); + }, + + /** + * Render notification. + * + * @since 4.9.0 + * + * @return {jQuery} Notification container. + */ + render: function() { + var notification = this, li, data, takeOverButton, request; + data = _.extend( + { + allowOverride: false, + returnUrl: api.settings.url['return'], + previewUrl: api.previewer.previewUrl.get(), + frontendPreviewUrl: api.previewer.getFrontendPreviewUrl() + }, + this + ); + + li = api.OverlayNotification.prototype.render.call( data ); + + // Try to autosave the changeset now. + api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) { + if ( ! response.autosaved ) { + li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail ); + } + } ); + + takeOverButton = li.find( '.customize-notice-take-over-button' ); + takeOverButton.on( 'click', function( event ) { + event.preventDefault(); + if ( request ) { + return; + } + + takeOverButton.addClass( 'disabled' ); + request = wp.ajax.post( 'customize_override_changeset_lock', { + wp_customize: 'on', + customize_theme: api.settings.theme.stylesheet, + customize_changeset_uuid: api.settings.changeset.uuid, + nonce: api.settings.nonce.override_lock + } ); + + request.done( function() { + api.notifications.remove( notification.code ); // Remove self. + api.state( 'changesetLocked' ).set( false ); + } ); + + request.fail( function( response ) { + var message = response.message || api.l10n.unknownRequestFail; + li.find( '.notice-error' ).prop( 'hidden', false ).text( message ); + + request.always( function() { + takeOverButton.removeClass( 'disabled' ); + } ); + } ); + + request.always( function() { + request = null; + } ); + } ); + + return li; + } + }); + + /** + * Start lock. + * + * @since 4.9.0 + * + * @param {Object} [args] - Args. + * @param {Object} [args.lockUser] - Lock user data. + * @param {boolean} [args.allowOverride=false] - Whether override is allowed. + * @return {void} + */ + function startLock( args ) { + if ( args && args.lockUser ) { + api.settings.changeset.lockUser = args.lockUser; + } + api.state( 'changesetLocked' ).set( true ); + api.notifications.add( new LockedNotification( 'changeset_locked', { + lockUser: api.settings.changeset.lockUser, + allowOverride: Boolean( args && args.allowOverride ) + } ) ); + } + + // Show initial notification. + if ( api.settings.changeset.lockUser ) { + startLock( { allowOverride: true } ); + } + + // Check for lock when sending heartbeat requests. + $( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) { + data.check_changeset_lock = true; + data.changeset_uuid = api.settings.changeset.uuid; + } ); + + // Handle heartbeat ticks. + $( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) { + var notification, code = 'changeset_locked'; + if ( ! data.customize_changeset_lock_user ) { + return; + } + + // Update notification when a different user takes over. + notification = api.notifications( code ); + if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) { + api.notifications.remove( code ); + } + + startLock( { + lockUser: data.customize_changeset_lock_user + } ); + } ); + + // Handle locking in response to changeset save errors. + api.bind( 'error', function( response ) { + if ( 'changeset_locked' === response.code && response.lock_user ) { + startLock( { + lockUser: response.lock_user + } ); + } + } ); + } )(); + + // Set up initial notifications. + (function() { + var removedQueryParams = [], autosaveDismissed = false; + + /** + * Obtain the URL to restore the autosave. + * + * @return {string} Customizer URL. + */ + function getAutosaveRestorationUrl() { + var urlParser, queryParams; + urlParser = document.createElement( 'a' ); + urlParser.href = location.href; + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + if ( api.settings.changeset.latestAutoDraftUuid ) { + queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid; + } else { + queryParams.customize_autosaved = 'on'; + } + queryParams['return'] = api.settings.url['return']; + urlParser.search = $.param( queryParams ); + return urlParser.href; + } + + /** + * Remove parameter from the URL. + * + * @param {Array} params - Parameter names to remove. + * @return {void} + */ + function stripParamsFromLocation( params ) { + var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0; + urlParser.href = location.href; + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + _.each( params, function( param ) { + if ( 'undefined' !== typeof queryParams[ param ] ) { + strippedParams += 1; + delete queryParams[ param ]; + } + } ); + if ( 0 === strippedParams ) { + return; + } + + urlParser.search = $.param( queryParams ); + history.replaceState( {}, document.title, urlParser.href ); + } + + /** + * Displays a Site Editor notification when a block theme is activated. + * + * @since 4.9.0 + * + * @param {string} [notification] - A notification to display. + * @return {void} + */ + function addSiteEditorNotification( notification ) { + api.notifications.add( new api.Notification( 'site_editor_block_theme_notice', { + message: notification, + type: 'info', + dismissible: false, + render: function() { + var notification = api.Notification.prototype.render.call( this ), + button = notification.find( 'button.switch-to-editor' ); + + button.on( 'click', function( event ) { + event.preventDefault(); + location.assign( button.data( 'action' ) ); + } ); + + return notification; + } + } ) ); + } + + /** + * Dismiss autosave. + * + * @return {void} + */ + function dismissAutosave() { + if ( autosaveDismissed ) { + return; + } + wp.ajax.post( 'customize_dismiss_autosave_or_lock', { + wp_customize: 'on', + customize_theme: api.settings.theme.stylesheet, + customize_changeset_uuid: api.settings.changeset.uuid, + nonce: api.settings.nonce.dismiss_autosave_or_lock, + dismiss_autosave: true + } ); + autosaveDismissed = true; + } + + /** + * Add notification regarding the availability of an autosave to restore. + * + * @return {void} + */ + function addAutosaveRestoreNotification() { + var code = 'autosave_available', onStateChange; + + // Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version. + api.notifications.add( new api.Notification( code, { + message: api.l10n.autosaveNotice, + type: 'warning', + dismissible: true, + render: function() { + var li = api.Notification.prototype.render.call( this ), link; + + // Handle clicking on restoration link. + link = li.find( 'a' ); + link.prop( 'href', getAutosaveRestorationUrl() ); + link.on( 'click', function( event ) { + event.preventDefault(); + location.replace( getAutosaveRestorationUrl() ); + } ); + + // Handle dismissal of notice. + li.find( '.notice-dismiss' ).on( 'click', dismissAutosave ); + + return li; + } + } ) ); + + // Remove the notification once the user starts making changes. + onStateChange = function() { + dismissAutosave(); + api.notifications.remove( code ); + api.unbind( 'change', onStateChange ); + api.state( 'changesetStatus' ).unbind( onStateChange ); + }; + api.bind( 'change', onStateChange ); + api.state( 'changesetStatus' ).bind( onStateChange ); + } + + if ( api.settings.changeset.autosaved ) { + api.state( 'saved' ).set( false ); + removedQueryParams.push( 'customize_autosaved' ); + } + if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) { + removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft. + } + if ( removedQueryParams.length > 0 ) { + stripParamsFromLocation( removedQueryParams ); + } + if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) { + addAutosaveRestoreNotification(); + } + var shouldDisplayBlockThemeNotification = !! parseInt( $( '#customize-info' ).data( 'block-theme' ), 10 ); + if (shouldDisplayBlockThemeNotification) { + addSiteEditorNotification( api.l10n.blockThemeNotification ); + } + })(); + + // Check if preview url is valid and load the preview frame. + if ( api.previewer.previewUrl() ) { + api.previewer.refresh(); + } else { + api.previewer.previewUrl( api.settings.url.home ); + } + + // Button bindings. + saveBtn.on( 'click', function( event ) { + api.previewer.save(); + event.preventDefault(); + }).on( 'keydown', function( event ) { + if ( 9 === event.which ) { // Tab. + return; + } + if ( 13 === event.which ) { // Enter. + api.previewer.save(); + } + event.preventDefault(); + }); + + closeBtn.on( 'keydown', function( event ) { + if ( 9 === event.which ) { // Tab. + return; + } + if ( 13 === event.which ) { // Enter. + this.click(); + } + event.preventDefault(); + }); + + $( '.collapse-sidebar' ).on( 'click', function() { + api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() ); + }); + + api.state( 'paneVisible' ).bind( function( paneVisible ) { + overlay.toggleClass( 'preview-only', ! paneVisible ); + overlay.toggleClass( 'expanded', paneVisible ); + overlay.toggleClass( 'collapsed', ! paneVisible ); + + if ( ! paneVisible ) { + $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar }); + } else { + $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar }); + } + }); + + // Keyboard shortcuts - esc to exit section/panel. + body.on( 'keydown', function( event ) { + var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = []; + + if ( 27 !== event.which ) { // Esc. + return; + } + + /* + * Abort if the event target is not the body (the default) and not inside of #customize-controls. + * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else. + */ + if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) { + return; + } + + // Abort if we're inside of a block editor instance. + if ( event.target.closest( '.block-editor-writing-flow' ) !== null || + event.target.closest( '.block-editor-block-list__block-popover' ) !== null + ) { + return; + } + + // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels. + api.control.each( function( control ) { + if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) { + expandedControls.push( control ); + } + }); + api.section.each( function( section ) { + if ( section.expanded() ) { + expandedSections.push( section ); + } + }); + api.panel.each( function( panel ) { + if ( panel.expanded() ) { + expandedPanels.push( panel ); + } + }); + + // Skip collapsing expanded controls if there are no expanded sections. + if ( expandedControls.length > 0 && 0 === expandedSections.length ) { + expandedControls.length = 0; + } + + // Collapse the most granular expanded object. + collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0]; + if ( collapsedObject ) { + if ( 'themes' === collapsedObject.params.type ) { + + // Themes panel or section. + if ( body.hasClass( 'modal-open' ) ) { + collapsedObject.closeDetails(); + } else if ( api.panel.has( 'themes' ) ) { + + // If we're collapsing a section, collapse the panel also. + api.panel( 'themes' ).collapse(); + } + return; + } + collapsedObject.collapse(); + event.preventDefault(); + } + }); + + $( '.customize-controls-preview-toggle' ).on( 'click', function() { + api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() ); + }); + + /* + * Sticky header feature. + */ + (function initStickyHeaders() { + var parentContainer = $( '.wp-full-overlay-sidebar-content' ), + changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader, + activeHeader, lastScrollTop; + + /** + * Determine which panel or section is currently expanded. + * + * @since 4.7.0 + * @access private + * + * @param {wp.customize.Panel|wp.customize.Section} container Construct. + * @return {void} + */ + changeContainer = function( container ) { + var newInstance = container, + expandedSection = api.state( 'expandedSection' ).get(), + expandedPanel = api.state( 'expandedPanel' ).get(), + headerElement; + + if ( activeHeader && activeHeader.element ) { + // Release previously active header element. + releaseStickyHeader( activeHeader.element ); + + // Remove event listener in the previous panel or section. + activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight ); + } + + if ( ! newInstance ) { + if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) { + newInstance = expandedPanel; + } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) { + newInstance = expandedSection; + } else { + activeHeader = false; + return; + } + } + + headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first(); + if ( headerElement.length ) { + activeHeader = { + instance: newInstance, + element: headerElement, + parent: headerElement.closest( '.customize-pane-child' ), + height: headerElement.outerHeight() + }; + + // Update header height whenever help text is expanded or collapsed. + activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight ); + + if ( expandedSection ) { + resetStickyHeader( activeHeader.element, activeHeader.parent ); + } + } else { + activeHeader = false; + } + }; + api.state( 'expandedSection' ).bind( changeContainer ); + api.state( 'expandedPanel' ).bind( changeContainer ); + + // Throttled scroll event handler. + parentContainer.on( 'scroll', _.throttle( function() { + if ( ! activeHeader ) { + return; + } + + var scrollTop = parentContainer.scrollTop(), + scrollDirection; + + if ( ! lastScrollTop ) { + scrollDirection = 1; + } else { + if ( scrollTop === lastScrollTop ) { + scrollDirection = 0; + } else if ( scrollTop > lastScrollTop ) { + scrollDirection = 1; + } else { + scrollDirection = -1; + } + } + lastScrollTop = scrollTop; + if ( 0 !== scrollDirection ) { + positionStickyHeader( activeHeader, scrollTop, scrollDirection ); + } + }, 8 ) ); + + // Update header position on sidebar layout change. + api.notifications.bind( 'sidebarTopUpdated', function() { + if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) { + activeHeader.element.css( 'top', parentContainer.css( 'top' ) ); + } + }); + + // Release header element if it is sticky. + releaseStickyHeader = function( headerElement ) { + if ( ! headerElement.hasClass( 'is-sticky' ) ) { + return; + } + headerElement + .removeClass( 'is-sticky' ) + .addClass( 'maybe-sticky is-in-view' ) + .css( 'top', parentContainer.scrollTop() + 'px' ); + }; + + // Reset position of the sticky header. + resetStickyHeader = function( headerElement, headerParent ) { + if ( headerElement.hasClass( 'is-in-view' ) ) { + headerElement + .removeClass( 'maybe-sticky is-in-view' ) + .css( { + width: '', + top: '' + } ); + headerParent.css( 'padding-top', '' ); + } + }; + + /** + * Update active header height. + * + * @since 4.7.0 + * @access private + * + * @return {void} + */ + updateHeaderHeight = function() { + activeHeader.height = activeHeader.element.outerHeight(); + }; + + /** + * Reposition header on throttled `scroll` event. + * + * @since 4.7.0 + * @access private + * + * @param {Object} header - Header. + * @param {number} scrollTop - Scroll top. + * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down. + * @return {void} + */ + positionStickyHeader = function( header, scrollTop, scrollDirection ) { + var headerElement = header.element, + headerParent = header.parent, + headerHeight = header.height, + headerTop = parseInt( headerElement.css( 'top' ), 10 ), + maybeSticky = headerElement.hasClass( 'maybe-sticky' ), + isSticky = headerElement.hasClass( 'is-sticky' ), + isInView = headerElement.hasClass( 'is-in-view' ), + isScrollingUp = ( -1 === scrollDirection ); + + // When scrolling down, gradually hide sticky header. + if ( ! isScrollingUp ) { + if ( isSticky ) { + headerTop = scrollTop; + headerElement + .removeClass( 'is-sticky' ) + .css( { + top: headerTop + 'px', + width: '' + } ); + } + if ( isInView && scrollTop > headerTop + headerHeight ) { + headerElement.removeClass( 'is-in-view' ); + headerParent.css( 'padding-top', '' ); + } + return; + } + + // Scrolling up. + if ( ! maybeSticky && scrollTop >= headerHeight ) { + maybeSticky = true; + headerElement.addClass( 'maybe-sticky' ); + } else if ( 0 === scrollTop ) { + // Reset header in base position. + headerElement + .removeClass( 'maybe-sticky is-in-view is-sticky' ) + .css( { + top: '', + width: '' + } ); + headerParent.css( 'padding-top', '' ); + return; + } + + if ( isInView && ! isSticky ) { + // Header is in the view but is not yet sticky. + if ( headerTop >= scrollTop ) { + // Header is fully visible. + headerElement + .addClass( 'is-sticky' ) + .css( { + top: parentContainer.css( 'top' ), + width: headerParent.outerWidth() + 'px' + } ); + } + } else if ( maybeSticky && ! isInView ) { + // Header is out of the view. + headerElement + .addClass( 'is-in-view' ) + .css( 'top', ( scrollTop - headerHeight ) + 'px' ); + headerParent.css( 'padding-top', headerHeight + 'px' ); + } + }; + }()); + + // Previewed device bindings. (The api.previewedDevice property + // is how this Value was first introduced, but since it has moved to api.state.) + api.previewedDevice = api.state( 'previewedDevice' ); + + // Set the default device. + api.bind( 'ready', function() { + _.find( api.settings.previewableDevices, function( value, key ) { + if ( true === value['default'] ) { + api.previewedDevice.set( key ); + return true; + } + } ); + } ); + + // Set the toggled device. + footerActions.find( '.devices button' ).on( 'click', function( event ) { + api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) ); + }); + + // Bind device changes. + api.previewedDevice.bind( function( newDevice ) { + var overlay = $( '.wp-full-overlay' ), + devices = ''; + + footerActions.find( '.devices button' ) + .removeClass( 'active' ) + .attr( 'aria-pressed', false ); + + footerActions.find( '.devices .preview-' + newDevice ) + .addClass( 'active' ) + .attr( 'aria-pressed', true ); + + $.each( api.settings.previewableDevices, function( device ) { + devices += ' preview-' + device; + } ); + + overlay + .removeClass( devices ) + .addClass( 'preview-' + newDevice ); + } ); + + // Bind site title display to the corresponding field. + if ( title.length ) { + api( 'blogname', function( setting ) { + var updateTitle = function() { + var blogTitle = setting() || ''; + title.text( blogTitle.toString().trim() || api.l10n.untitledBlogName ); + }; + setting.bind( updateTitle ); + updateTitle(); + } ); + } + + /* + * Create a postMessage connection with a parent frame, + * in case the Customizer frame was opened with the Customize loader. + * + * @see wp.customize.Loader + */ + parent = new api.Messenger({ + url: api.settings.url.parent, + channel: 'loader' + }); + + // Handle exiting of Customizer. + (function() { + var isInsideIframe = false; + + function isCleanState() { + var defaultChangesetStatus; + + /* + * Handle special case of previewing theme switch since some settings (for nav menus and widgets) + * are pre-dirty and non-active themes can only ever be auto-drafts. + */ + if ( ! api.state( 'activated' ).get() ) { + return 0 === api._latestRevision; + } + + // Dirty if the changeset status has been changed but not saved yet. + defaultChangesetStatus = api.state( 'changesetStatus' ).get(); + if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) { + defaultChangesetStatus = 'publish'; + } + if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) { + return false; + } + + // Dirty if scheduled but the changeset date hasn't been saved yet. + if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) { + return false; + } + + return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get(); + } + + /* + * If we receive a 'back' event, we're inside an iframe. + * Send any clicks to the 'Return' link to the parent page. + */ + parent.bind( 'back', function() { + isInsideIframe = true; + }); + + function startPromptingBeforeUnload() { + api.unbind( 'change', startPromptingBeforeUnload ); + api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload ); + api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload ); + + // Prompt user with AYS dialog if leaving the Customizer with unsaved changes. + $( window ).on( 'beforeunload.customize-confirm', function() { + if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) { + setTimeout( function() { + overlay.removeClass( 'customize-loading' ); + }, 1 ); + return api.l10n.saveAlert; + } + }); + } + api.bind( 'change', startPromptingBeforeUnload ); + api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload ); + api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload ); + + function requestClose() { + var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false; + + if ( isCleanState() ) { + dismissLock = true; + } else if ( confirm( api.l10n.saveAlert ) ) { + + dismissLock = true; + + // Mark all settings as clean to prevent another call to requestChangesetUpdate. + api.each( function( setting ) { + setting._dirty = false; + }); + $( document ).off( 'visibilitychange.wp-customize-changeset-update' ); + $( window ).off( 'beforeunload.wp-customize-changeset-update' ); + + closeBtn.css( 'cursor', 'progress' ); + if ( '' !== api.state( 'changesetStatus' ).get() ) { + dismissAutoSave = true; + } + } else { + clearedToClose.reject(); + } + + if ( dismissLock || dismissAutoSave ) { + wp.ajax.send( 'customize_dismiss_autosave_or_lock', { + timeout: 500, // Don't wait too long. + data: { + wp_customize: 'on', + customize_theme: api.settings.theme.stylesheet, + customize_changeset_uuid: api.settings.changeset.uuid, + nonce: api.settings.nonce.dismiss_autosave_or_lock, + dismiss_autosave: dismissAutoSave, + dismiss_lock: dismissLock + } + } ).always( function() { + clearedToClose.resolve(); + } ); + } + + return clearedToClose.promise(); + } + + parent.bind( 'confirm-close', function() { + requestClose().done( function() { + parent.send( 'confirmed-close', true ); + } ).fail( function() { + parent.send( 'confirmed-close', false ); + } ); + } ); + + closeBtn.on( 'click.customize-controls-close', function( event ) { + event.preventDefault(); + if ( isInsideIframe ) { + parent.send( 'close' ); // See confirm-close logic above. + } else { + requestClose().done( function() { + $( window ).off( 'beforeunload.customize-confirm' ); + window.location.href = closeBtn.prop( 'href' ); + } ); + } + }); + })(); + + // Pass events through to the parent. + $.each( [ 'saved', 'change' ], function ( i, event ) { + api.bind( event, function() { + parent.send( event ); + }); + } ); + + // Pass titles to the parent. + api.bind( 'title', function( newTitle ) { + parent.send( 'title', newTitle ); + }); + + if ( api.settings.changeset.branching ) { + parent.send( 'changeset-uuid', api.settings.changeset.uuid ); + } + + // Initialize the connection with the parent frame. + parent.send( 'ready' ); + + // Control visibility for default controls. + $.each({ + 'background_image': { + controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], + callback: function( to ) { return !! to; } + }, + 'show_on_front': { + controls: [ 'page_on_front', 'page_for_posts' ], + callback: function( to ) { return 'page' === to; } + }, + 'header_textcolor': { + controls: [ 'header_textcolor' ], + callback: function( to ) { return 'blank' !== to; } + } + }, function( settingId, o ) { + api( settingId, function( setting ) { + $.each( o.controls, function( i, controlId ) { + api.control( controlId, function( control ) { + var visibility = function( to ) { + control.container.toggle( o.callback( to ) ); + }; + + visibility( setting.get() ); + setting.bind( visibility ); + }); + }); + }); + }); + + api.control( 'background_preset', function( control ) { + var visibility, defaultValues, values, toggleVisibility, updateSettings, preset; + + visibility = { // position, size, repeat, attachment. + 'default': [ false, false, false, false ], + 'fill': [ true, false, false, false ], + 'fit': [ true, false, true, false ], + 'repeat': [ true, false, false, true ], + 'custom': [ true, true, true, true ] + }; + + defaultValues = [ + _wpCustomizeBackground.defaults['default-position-x'], + _wpCustomizeBackground.defaults['default-position-y'], + _wpCustomizeBackground.defaults['default-size'], + _wpCustomizeBackground.defaults['default-repeat'], + _wpCustomizeBackground.defaults['default-attachment'] + ]; + + values = { // position_x, position_y, size, repeat, attachment. + 'default': defaultValues, + 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ], + 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ], + 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ] + }; + + // @todo These should actually toggle the active state, + // but without the preview overriding the state in data.activeControls. + toggleVisibility = function( preset ) { + _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) { + var control = api.control( controlId ); + if ( control ) { + control.container.toggle( visibility[ preset ][ i ] ); + } + } ); + }; + + updateSettings = function( preset ) { + _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) { + var setting = api( settingId ); + if ( setting ) { + setting.set( values[ preset ][ i ] ); + } + } ); + }; + + preset = control.setting.get(); + toggleVisibility( preset ); + + control.setting.bind( 'change', function( preset ) { + toggleVisibility( preset ); + if ( 'custom' !== preset ) { + updateSettings( preset ); + } + } ); + } ); + + api.control( 'background_repeat', function( control ) { + control.elements[0].unsync( api( 'background_repeat' ) ); + + control.element = new api.Element( control.container.find( 'input' ) ); + control.element.set( 'no-repeat' !== control.setting() ); + + control.element.bind( function( to ) { + control.setting.set( to ? 'repeat' : 'no-repeat' ); + } ); + + control.setting.bind( function( to ) { + control.element.set( 'no-repeat' !== to ); + } ); + } ); + + api.control( 'background_attachment', function( control ) { + control.elements[0].unsync( api( 'background_attachment' ) ); + + control.element = new api.Element( control.container.find( 'input' ) ); + control.element.set( 'fixed' !== control.setting() ); + + control.element.bind( function( to ) { + control.setting.set( to ? 'scroll' : 'fixed' ); + } ); + + control.setting.bind( function( to ) { + control.element.set( 'fixed' !== to ); + } ); + } ); + + // Juggle the two controls that use header_textcolor. + api.control( 'display_header_text', function( control ) { + var last = ''; + + control.elements[0].unsync( api( 'header_textcolor' ) ); + + control.element = new api.Element( control.container.find('input') ); + control.element.set( 'blank' !== control.setting() ); + + control.element.bind( function( to ) { + if ( ! to ) { + last = api( 'header_textcolor' ).get(); + } + + control.setting.set( to ? last : 'blank' ); + }); + + control.setting.bind( function( to ) { + control.element.set( 'blank' !== to ); + }); + }); + + // Add behaviors to the static front page controls. + api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) { + var handleChange = function() { + var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision'; + pageOnFrontId = parseInt( pageOnFront(), 10 ); + pageForPostsId = parseInt( pageForPosts(), 10 ); + + if ( 'page' === showOnFront() ) { + + // Change previewed URL to the homepage when changing the page_on_front. + if ( setting === pageOnFront && pageOnFrontId > 0 ) { + api.previewer.previewUrl.set( api.settings.url.home ); + } + + // Change the previewed URL to the selected page when changing the page_for_posts. + if ( setting === pageForPosts && pageForPostsId > 0 ) { + api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId ); + } + } + + // Toggle notification when the homepage and posts page are both set and the same. + if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) { + showOnFront.notifications.add( new api.Notification( errorCode, { + type: 'error', + message: api.l10n.pageOnFrontError + } ) ); + } else { + showOnFront.notifications.remove( errorCode ); + } + }; + showOnFront.bind( handleChange ); + pageOnFront.bind( handleChange ); + pageForPosts.bind( handleChange ); + handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset. + + // Move notifications container to the bottom. + api.control( 'show_on_front', function( showOnFrontControl ) { + showOnFrontControl.deferred.embedded.done( function() { + showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() ); + }); + }); + }); + + // Add code editor for Custom CSS. + (function() { + var sectionReady = $.Deferred(); + + api.section( 'custom_css', function( section ) { + section.deferred.embedded.done( function() { + if ( section.expanded() ) { + sectionReady.resolve( section ); + } else { + section.expanded.bind( function( isExpanded ) { + if ( isExpanded ) { + sectionReady.resolve( section ); + } + } ); + } + }); + }); + + // Set up the section description behaviors. + sectionReady.done( function setupSectionDescription( section ) { + var control = api.control( 'custom_css' ); + + // Hide redundant label for visual users. + control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' ); + + // Close the section description when clicking the close button. + section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() { + section.container.find( '.section-meta .customize-section-description:first' ) + .removeClass( 'open' ) + .slideUp(); + + section.container.find( '.customize-help-toggle' ) + .attr( 'aria-expanded', 'false' ) + .focus(); // Avoid focus loss. + }); + + // Reveal help text if setting is empty. + if ( control && ! control.setting.get() ) { + section.container.find( '.section-meta .customize-section-description:first' ) + .addClass( 'open' ) + .show() + .trigger( 'toggled' ); + + section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' ); + } + }); + })(); + + // Toggle visibility of Header Video notice when active state change. + api.control( 'header_video', function( headerVideoControl ) { + headerVideoControl.deferred.embedded.done( function() { + var toggleNotice = function() { + var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available'; + if ( ! section ) { + return; + } + if ( headerVideoControl.active.get() ) { + section.notifications.remove( noticeCode ); + } else { + section.notifications.add( new api.Notification( noticeCode, { + type: 'info', + message: api.l10n.videoHeaderNotice + } ) ); + } + }; + toggleNotice(); + headerVideoControl.active.bind( toggleNotice ); + } ); + } ); + + // Update the setting validities. + api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) { + api._handleSettingValidities( { + settingValidities: settingValidities, + focusInvalidControl: false + } ); + } ); + + // Focus on the control that is associated with the given setting. + api.previewer.bind( 'focus-control-for-setting', function( settingId ) { + var matchedControls = []; + api.control.each( function( control ) { + var settingIds = _.pluck( control.settings, 'id' ); + if ( -1 !== _.indexOf( settingIds, settingId ) ) { + matchedControls.push( control ); + } + } ); + + // Focus on the matched control with the lowest priority (appearing higher). + if ( matchedControls.length ) { + matchedControls.sort( function( a, b ) { + return a.priority() - b.priority(); + } ); + matchedControls[0].focus(); + } + } ); + + // Refresh the preview when it requests. + api.previewer.bind( 'refresh', function() { + api.previewer.refresh(); + }); + + // Update the edit shortcut visibility state. + api.state( 'paneVisible' ).bind( function( isPaneVisible ) { + var isMobileScreen; + if ( window.matchMedia ) { + isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches; + } else { + isMobileScreen = $( window ).width() <= 640; + } + api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' ); + } ); + if ( window.matchMedia ) { + window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() { + var state = api.state( 'paneVisible' ); + state.callbacks.fireWith( state, [ state.get(), state.get() ] ); + } ); + } + api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) { + api.state( 'editShortcutVisibility' ).set( visibility ); + } ); + api.state( 'editShortcutVisibility' ).bind( function( visibility ) { + api.previewer.send( 'edit-shortcut-visibility', visibility ); + } ); + + // Autosave changeset. + function startAutosaving() { + var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false; + + api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once. + + function onChangeSaved( isSaved ) { + if ( ! isSaved && ! api.settings.changeset.autosaved ) { + api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in. + api.previewer.send( 'autosaving' ); + } + } + api.state( 'saved' ).bind( onChangeSaved ); + onChangeSaved( api.state( 'saved' ).get() ); + + /** + * Request changeset update and then re-schedule the next changeset update time. + * + * @since 4.7.0 + * @private + */ + updateChangesetWithReschedule = function() { + if ( ! updatePending ) { + updatePending = true; + api.requestChangesetUpdate( {}, { autosave: true } ).always( function() { + updatePending = false; + } ); + } + scheduleChangesetUpdate(); + }; + + /** + * Schedule changeset update. + * + * @since 4.7.0 + * @private + */ + scheduleChangesetUpdate = function() { + clearTimeout( timeoutId ); + timeoutId = setTimeout( function() { + updateChangesetWithReschedule(); + }, api.settings.timeouts.changesetAutoSave ); + }; + + // Start auto-save interval for updating changeset. + scheduleChangesetUpdate(); + + // Save changeset when focus removed from window. + $( document ).on( 'visibilitychange.wp-customize-changeset-update', function() { + if ( document.hidden ) { + updateChangesetWithReschedule(); + } + } ); + + // Save changeset before unloading window. + $( window ).on( 'beforeunload.wp-customize-changeset-update', function() { + updateChangesetWithReschedule(); + } ); + } + api.bind( 'change', startAutosaving ); + + // Make sure TinyMCE dialogs appear above Customizer UI. + $( document ).one( 'tinymce-editor-setup', function() { + if ( window.tinymce.ui.FloatPanel && ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) ) { + window.tinymce.ui.FloatPanel.zIndex = 500001; + } + } ); + + body.addClass( 'ready' ); + api.trigger( 'ready' ); + }); + +})( wp, jQuery ); diff --git a/wp-admin/js/customize-controls.min.js b/wp-admin/js/customize-controls.min.js new file mode 100644 index 0000000..4f3efbf --- /dev/null +++ b/wp-admin/js/customize-controls.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(J){var a,s,t,e,n,i,Y=wp.customize,o=window.matchMedia("(prefers-reduced-motion: reduce)"),r=o.matches;o.addEventListener("change",function(e){r=e.matches}),Y.OverlayNotification=Y.Notification.extend({loading:!1,initialize:function(e,t){var n=this;Y.Notification.prototype.initialize.call(n,e,t),n.containerClasses+=" notification-overlay",n.loading&&(n.containerClasses+=" notification-loading")},render:function(){var e=Y.Notification.prototype.render.call(this);return e.on("keydown",_.bind(this.handleEscape,this)),e},handleEscape:function(e){var t=this;27===e.which&&(e.stopPropagation(),t.dismissible)&&t.parent&&t.parent.remove(t.code)}}),Y.Notifications=Y.Values.extend({alt:!1,defaultConstructor:Y.Notification,initialize:function(e){var t=this;Y.Values.prototype.initialize.call(t,e),_.bindAll(t,"constrainFocus"),t._addedIncrement=0,t._addedOrder={},t.bind("add",function(e){t.trigger("change",e)}),t.bind("removed",function(e){t.trigger("change",e)})},count:function(){return _.size(this._value)},add:function(e,t){var n,i=this,t="string"==typeof e?(n=e,t):(n=e.code,e);return i.has(n)||(i._addedIncrement+=1,i._addedOrder[n]=i._addedIncrement),Y.Values.prototype.add.call(i,n,t)},remove:function(e){return delete this._addedOrder[e],Y.Values.prototype.remove.call(this,e)},get:function(e){var a,o=this,t=_.values(o._value);return _.extend({sort:!1},e).sort&&(a={error:4,warning:3,success:2,info:1},t.sort(function(e,t){var n=0,i=0;return(n=_.isUndefined(a[e.type])?n:a[e.type])!==(i=_.isUndefined(a[t.type])?i:a[t.type])?i-n:o._addedOrder[t.code]-o._addedOrder[e.code]})),t},render:function(){var e,t,n,i=this,a=!1,o=[],s={};i.container&&i.container.length&&(e=i.get({sort:!0}),i.container.toggle(0!==e.length),i.container.is(i.previousContainer)&&_.isEqual(e,i.previousNotifications)||((n=i.container.children("ul").first()).length||(n=J("<ul></ul>"),i.container.append(n)),n.find("> [data-code]").remove(),_.each(i.previousNotifications,function(e){s[e.code]=e}),_.each(e,function(e){var t;!wp.a11y||s[e.code]&&_.isEqual(e.message,s[e.code].message)||wp.a11y.speak(e.message,"assertive"),t=J(e.render()),e.container=t,n.append(t),e.extended(Y.OverlayNotification)&&o.push(e)}),(t=Boolean(o.length))!==(a=i.previousNotifications?Boolean(_.find(i.previousNotifications,function(e){return e.extended(Y.OverlayNotification)})):a)&&(J(document.body).toggleClass("customize-loading",t),i.container.toggleClass("has-overlay-notifications",t),t?(i.previousActiveElement=document.activeElement,J(document).on("keydown",i.constrainFocus)):J(document).off("keydown",i.constrainFocus)),t?(i.focusContainer=o[o.length-1].container,i.focusContainer.prop("tabIndex",-1),((a=i.focusContainer.find(":focusable")).length?a.first():i.focusContainer).focus()):i.previousActiveElement&&(J(i.previousActiveElement).trigger("focus"),i.previousActiveElement=null),i.previousNotifications=e,i.previousContainer=i.container,i.trigger("rendered")))},constrainFocus:function(e){var t,n=this;e.stopPropagation(),9===e.which&&(0===(t=n.focusContainer.find(":focusable")).length&&(t=n.focusContainer),!J.contains(n.focusContainer[0],e.target)||!J.contains(n.focusContainer[0],document.activeElement)||t.last().is(e.target)&&!e.shiftKey?(e.preventDefault(),t.first().focus()):t.first().is(e.target)&&e.shiftKey&&(e.preventDefault(),t.last().focus()))}}),Y.Setting=Y.Value.extend({defaults:{transport:"refresh",dirty:!1},initialize:function(e,t,n){var i=this,n=_.extend({previewer:Y.previewer},i.defaults,n||{});Y.Value.prototype.initialize.call(i,t,n),i.id=e,i._dirty=n.dirty,i.notifications=new Y.Notifications,i.bind(i.preview)},preview:function(){var e=this,t=e.transport;"postMessage"===(t="postMessage"!==t||Y.state("previewerAlive").get()?t:"refresh")?e.previewer.send("setting",[e.id,e()]):"refresh"===t&&e.previewer.refresh()},findControls:function(){var n=this,i=[];return Y.control.each(function(t){_.each(t.settings,function(e){e.id===n.id&&i.push(t)})}),i}}),Y._latestRevision=0,Y._lastSavedRevision=0,Y._latestSettingRevisions={},Y.bind("change",function(e){Y._latestRevision+=1,Y._latestSettingRevisions[e.id]=Y._latestRevision}),Y.bind("ready",function(){Y.bind("add",function(e){e._dirty&&(Y._latestRevision+=1,Y._latestSettingRevisions[e.id]=Y._latestRevision)})}),Y.dirtyValues=function(n){var i={};return Y.each(function(e){var t;e._dirty&&(t=Y._latestSettingRevisions[e.id],Y.state("changesetStatus").get()&&n&&n.unsaved&&(_.isUndefined(t)||t<=Y._lastSavedRevision)||(i[e.id]=e.get()))}),i},Y.requestChangesetUpdate=function(n,e){var t,i={},a=new J.Deferred;if(0!==Y.state("processing").get())a.reject("already_processing");else if(e=_.extend({title:null,date:null,autosave:!1,force:!1},e),n&&_.extend(i,n),_.each(Y.dirtyValues({unsaved:!0}),function(e,t){n&&null===n[t]||(i[t]=_.extend({},i[t]||{},{value:e}))}),Y.trigger("changeset-save",i,e),!e.force&&_.isEmpty(i)&&null===e.title&&null===e.date)a.resolve({});else{if(e.status)return a.reject({code:"illegal_status_in_changeset_update"}).promise();if(e.date&&e.autosave)return a.reject({code:"illegal_autosave_with_date_gmt"}).promise();Y.state("processing").set(Y.state("processing").get()+1),a.always(function(){Y.state("processing").set(Y.state("processing").get()-1)}),delete(t=Y.previewer.query({excludeCustomizedSaved:!0})).customized,_.extend(t,{nonce:Y.settings.nonce.save,customize_theme:Y.settings.theme.stylesheet,customize_changeset_data:JSON.stringify(i)}),null!==e.title&&(t.customize_changeset_title=e.title),null!==e.date&&(t.customize_changeset_date=e.date),!1!==e.autosave&&(t.customize_changeset_autosave="true"),Y.trigger("save-request-params",t),(e=wp.ajax.post("customize_save",t)).done(function(e){var n={};Y._lastSavedRevision=Math.max(Y._latestRevision,Y._lastSavedRevision),Y.state("changesetStatus").set(e.changeset_status),e.changeset_date&&Y.state("changesetDate").set(e.changeset_date),a.resolve(e),Y.trigger("changeset-saved",e),e.setting_validities&&_.each(e.setting_validities,function(e,t){!0===e&&_.isObject(i[t])&&!_.isUndefined(i[t].value)&&(n[t]=i[t].value)}),Y.previewer.send("changeset-saved",_.extend({},e,{saved_changeset_values:n}))}),e.fail(function(e){a.reject(e),Y.trigger("changeset-error",e)}),e.always(function(e){e.setting_validities&&Y._handleSettingValidities({settingValidities:e.setting_validities})})}return a.promise()},Y.utils.bubbleChildValueChanges=function(n,e){J.each(e,function(e,t){n[t].bind(function(e,t){n.parent&&e!==t&&n.parent.trigger("change",n)})})},o=function(e){var t,n,i=this,a=function(){var e;i.extended(Y.Panel)&&1<(n=i.sections()).length&&n.forEach(function(e){e.expanded()&&e.collapse()}),e=(i.extended(Y.Panel)||i.extended(Y.Section))&&i.expanded&&i.expanded()?i.contentContainer:i.container,(n=0===(n=e.find(".control-focus:first")).length?e.find("input, select, textarea, button, object, a[href], [tabindex]").filter(":visible").first():n).focus()};(e=e||{}).completeCallback?(t=e.completeCallback,e.completeCallback=function(){a(),t()}):e.completeCallback=a,Y.state("paneVisible").set(!0),i.expand?i.expand(e):e.completeCallback()},Y.utils.prioritySort=function(e,t){return e.priority()===t.priority()&&"number"==typeof e.params.instanceNumber&&"number"==typeof t.params.instanceNumber?e.params.instanceNumber-t.params.instanceNumber:e.priority()-t.priority()},Y.utils.isKeydownButNotEnterEvent=function(e){return"keydown"===e.type&&13!==e.which},Y.utils.areElementListsEqual=function(e,t){return e.length===t.length&&-1===_.indexOf(_.map(_.zip(e,t),function(e){return J(e[0]).is(e[1])}),!1)},Y.utils.highlightButton=function(e,t){var n,i="button-see-me",a=!1;function o(){a=!0}return(n=_.extend({delay:0,focusTarget:e},t)).focusTarget.on("focusin",o),setTimeout(function(){n.focusTarget.off("focusin",o),a||(e.addClass(i),e.one("animationend",function(){e.removeClass(i)}))},n.delay),o},Y.utils.getCurrentTimestamp=function(){var e=_.now(),t=new Date(Y.settings.initialServerDate.replace(/-/g,"/")),e=e-Y.settings.initialClientTimestamp;return e+=Y.settings.initialClientTimestamp-Y.settings.initialServerTimestamp,t.setTime(t.getTime()+e),t.getTime()},Y.utils.getRemainingTime=function(e){e=e instanceof Date?e.getTime():"string"==typeof e?new Date(e.replace(/-/g,"/")).getTime():e,e-=Y.utils.getCurrentTimestamp();return Math.ceil(e/1e3)},t=document.createElement("div"),e={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"},n=_.find(_.keys(e),function(e){return!_.isUndefined(t.style[e])}),s=n?e[n]:null,a=Y.Class.extend({defaultActiveArguments:{duration:"fast",completeCallback:J.noop},defaultExpandedArguments:{duration:"fast",completeCallback:J.noop},containerType:"container",defaults:{title:"",description:"",priority:100,type:"default",content:null,active:!0,instanceNumber:null},initialize:function(e,t){var n=this;n.id=e,a.instanceCounter||(a.instanceCounter=0),a.instanceCounter++,J.extend(n,{params:_.defaults(t.params||t,n.defaults)}),n.params.instanceNumber||(n.params.instanceNumber=a.instanceCounter),n.notifications=new Y.Notifications,n.templateSelector=n.params.templateId||"customize-"+n.containerType+"-"+n.params.type,n.container=J(n.params.content),0===n.container.length&&(n.container=J(n.getContainer())),n.headContainer=n.container,n.contentContainer=n.getContent(),n.container=n.container.add(n.contentContainer),n.deferred={embedded:new J.Deferred},n.priority=new Y.Value,n.active=new Y.Value,n.activeArgumentsQueue=[],n.expanded=new Y.Value,n.expandedArgumentsQueue=[],n.active.bind(function(e){var t=n.activeArgumentsQueue.shift(),t=J.extend({},n.defaultActiveArguments,t);e=e&&n.isContextuallyActive(),n.onChangeActive(e,t)}),n.expanded.bind(function(e){var t=n.expandedArgumentsQueue.shift(),t=J.extend({},n.defaultExpandedArguments,t);n.onChangeExpanded(e,t)}),n.deferred.embedded.done(function(){n.setupNotifications(),n.attachEvents()}),Y.utils.bubbleChildValueChanges(n,["priority","active"]),n.priority.set(n.params.priority),n.active.set(n.params.active),n.expanded.set(!1)},getNotificationsContainerElement:function(){return this.contentContainer.find(".customize-control-notifications-container:first")},setupNotifications:function(){var e,t=this;t.notifications.container=t.getNotificationsContainerElement(),t.expanded.bind(e=function(){t.expanded.get()&&t.notifications.render()}),e(),t.notifications.bind("change",_.debounce(e))},ready:function(){},_children:function(t,e){var n=this,i=[];return Y[e].each(function(e){e[t].get()===n.id&&i.push(e)}),i.sort(Y.utils.prioritySort),i},isContextuallyActive:function(){throw new Error("Container.isContextuallyActive() must be overridden in a subclass.")},onChangeActive:function(e,t){var n,i=this,a=i.headContainer;t.unchanged?t.completeCallback&&t.completeCallback():(n="resolved"===Y.previewer.deferred.active.state()?t.duration:0,i.extended(Y.Panel)&&(Y.panel.each(function(e){e!==i&&e.expanded()&&(n=0)}),e||_.each(i.sections(),function(e){e.collapse({duration:0})})),J.contains(document,a.get(0))?e?a.slideDown(n,t.completeCallback):i.expanded()?i.collapse({duration:n,completeCallback:function(){a.slideUp(n,t.completeCallback)}}):a.slideUp(n,t.completeCallback):(a.toggle(e),t.completeCallback&&t.completeCallback()))},_toggleActive:function(e,t){return t=t||{},e&&this.active.get()||!e&&!this.active.get()?(t.unchanged=!0,this.onChangeActive(this.active.get(),t),!1):(t.unchanged=!1,this.activeArgumentsQueue.push(t),this.active.set(e),!0)},activate:function(e){return this._toggleActive(!0,e)},deactivate:function(e){return this._toggleActive(!1,e)},onChangeExpanded:function(){throw new Error("Must override with subclass.")},_toggleExpanded:function(e,t){var n,i=this;return n=(t=t||{}).completeCallback,!(e&&!i.active()||(Y.state("paneVisible").set(!0),t.completeCallback=function(){n&&n.apply(i,arguments),e?i.container.trigger("expanded"):i.container.trigger("collapsed")},e&&i.expanded.get()||!e&&!i.expanded.get()?(t.unchanged=!0,i.onChangeExpanded(i.expanded.get(),t),1):(t.unchanged=!1,i.expandedArgumentsQueue.push(t),i.expanded.set(e),0)))},expand:function(e){return this._toggleExpanded(!0,e)},collapse:function(e){return this._toggleExpanded(!1,e)},_animateChangeExpanded:function(t){var a,o,n,i;!s||r?_.defer(function(){t&&t()}):(o=(a=this).contentContainer,i=o.closest(".wp-full-overlay").add(o),a.panel&&""!==a.panel()&&!Y.panel(a.panel()).contentContainer.hasClass("skip-transition")||(i=i.add("#customize-info, .customize-pane-parent")),n=function(e){2===e.eventPhase&&J(e.target).is(o)&&(o.off(s,n),i.removeClass("busy"),t)&&t()},o.on(s,n),i.addClass("busy"),_.defer(function(){var e=o.closest(".wp-full-overlay-sidebar-content"),t=e.scrollTop(),n=o.data("previous-scrollTop")||0,i=a.expanded();i&&0<t?(o.css("top",t+"px"),o.data("previous-scrollTop",t)):!i&&0<t+n&&(o.css("top",n-t+"px"),e.scrollTop(n))}))},focus:o,getContainer:function(){var e=this,t=0!==J("#tmpl-"+e.templateSelector).length?wp.template(e.templateSelector):wp.template("customize-"+e.containerType+"-default");return t&&e.container?t(_.extend({id:e.id},e.params)).toString().trim():"<li></li>"},getContent:function(){var e=this.container,t=e.find(".accordion-section-content, .control-panel-content").first(),n="sub-"+e.attr("id"),i=n,a=e.attr("aria-owns");return e.attr("aria-owns",i=a?i+" "+a:i),t.detach().attr({id:n,class:"customize-pane-child "+t.attr("class")+" "+e.attr("class")})}}),Y.Section=a.extend({containerType:"section",containerParent:"#customize-theme-controls",containerPaneParent:".customize-pane-parent",defaults:{title:"",description:"",priority:100,type:"default",content:null,active:!0,instanceNumber:null,panel:null,customizeAction:""},initialize:function(e,t){var n=this,i=t.params||t;i.type||_.find(Y.sectionConstructor,function(e,t){return e===n.constructor&&(i.type=t,!0)}),a.prototype.initialize.call(n,e,i),n.id=e,n.panel=new Y.Value,n.panel.bind(function(e){J(n.headContainer).toggleClass("control-subsection",!!e)}),n.panel.set(n.params.panel||""),Y.utils.bubbleChildValueChanges(n,["panel"]),n.embed(),n.deferred.embedded.done(function(){n.ready()})},embed:function(){var e,n=this;n.containerParent=Y.ensure(n.containerParent),n.panel.bind(e=function(e){var t;e?Y.panel(e,function(e){e.deferred.embedded.done(function(){t=e.contentContainer,n.headContainer.parent().is(t)||t.append(n.headContainer),n.contentContainer.parent().is(n.headContainer)||n.containerParent.append(n.contentContainer),n.deferred.embedded.resolve()})}):(t=Y.ensure(n.containerPaneParent),n.headContainer.parent().is(t)||t.append(n.headContainer),n.contentContainer.parent().is(n.headContainer)||n.containerParent.append(n.contentContainer),n.deferred.embedded.resolve())}),e(n.panel.get())},attachEvents:function(){var e,t,n=this;n.container.hasClass("cannot-expand")||(n.container.find(".accordion-section-title, .customize-section-back").on("click keydown",function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),n.expanded()?n.collapse():n.expand())}),n.container.find(".customize-section-title .customize-help-toggle").on("click",function(){(e=n.container.find(".section-meta")).hasClass("cannot-expand")||((t=e.find(".customize-section-description:first")).toggleClass("open"),t.slideToggle(n.defaultExpandedArguments.duration,function(){t.trigger("toggled")}),J(this).attr("aria-expanded",function(e,t){return"true"===t?"false":"true"}))}))},isContextuallyActive:function(){var e=this.controls(),t=0;return _(e).each(function(e){e.active()&&(t+=1)}),0!==t},controls:function(){return this._children("section","control")},onChangeExpanded:function(e,t){var n,i=this,a=i.headContainer.closest(".wp-full-overlay-sidebar-content"),o=i.contentContainer,s=i.headContainer.closest(".wp-full-overlay"),r=o.find(".customize-section-back"),c=i.headContainer.find(".accordion-section-title").first();e&&!o.hasClass("open")?(n=t.unchanged?t.completeCallback:function(){i._animateChangeExpanded(function(){c.attr("tabindex","-1"),r.attr("tabindex","0"),r.trigger("focus"),o.css("top",""),a.scrollTop(0),t.completeCallback&&t.completeCallback()}),o.addClass("open"),s.addClass("section-open"),Y.state("expandedSection").set(i)}.bind(this),t.allowMultiple||Y.section.each(function(e){e!==i&&e.collapse({duration:t.duration})}),i.panel()?Y.panel(i.panel()).expand({duration:t.duration,completeCallback:n}):(t.allowMultiple||Y.panel.each(function(e){e.collapse()}),n())):!e&&o.hasClass("open")?(i.panel()&&(n=Y.panel(i.panel())).contentContainer.hasClass("skip-transition")&&n.collapse(),i._animateChangeExpanded(function(){r.attr("tabindex","-1"),c.attr("tabindex","0"),c.trigger("focus"),o.css("top",""),t.completeCallback&&t.completeCallback()}),o.removeClass("open"),s.removeClass("section-open"),i===Y.state("expandedSection").get()&&Y.state("expandedSection").set(!1)):t.completeCallback&&t.completeCallback()}}),Y.ThemesSection=Y.Section.extend({currentTheme:"",overlay:"",template:"",screenshotQueue:null,$window:null,$body:null,loaded:0,loading:!1,fullyLoaded:!1,term:"",tags:"",nextTerm:"",nextTags:"",filtersHeight:0,headerContainer:null,updateCountDebounced:null,initialize:function(e,t){var n=this;n.headerContainer=J(),n.$window=J(window),n.$body=J(document.body),Y.Section.prototype.initialize.call(n,e,t),n.updateCountDebounced=_.debounce(n.updateCount,500)},embed:function(){var n=this,e=function(e){var t;Y.panel(e,function(e){e.deferred.embedded.done(function(){t=e.contentContainer,n.headContainer.parent().is(t)||t.find(".customize-themes-full-container-container").before(n.headContainer),n.contentContainer.parent().is(n.headContainer)||n.containerParent.append(n.contentContainer),n.deferred.embedded.resolve()})})};n.panel.bind(e),e(n.panel.get())},ready:function(){var t=this;t.overlay=t.container.find(".theme-overlay"),t.template=wp.template("customize-themes-details-view"),t.container.on("keydown",function(e){t.overlay.find(".theme-wrap").is(":visible")&&(39===e.keyCode&&t.nextTheme(),37===e.keyCode&&t.previousTheme(),27===e.keyCode)&&(t.$body.hasClass("modal-open")?t.closeDetails():t.headerContainer.find(".customize-themes-section-title").focus(),e.stopPropagation())}),t.renderScreenshots=_.throttle(t.renderScreenshots,100),_.bindAll(t,"renderScreenshots","loadMore","checkTerm","filtersChecked")},isContextuallyActive:function(){return this.active()},attachEvents:function(){var e,n=this;function t(){var e=n.headerContainer.find(".customize-themes-section-title");e.toggleClass("selected",n.expanded()),e.attr("aria-expanded",n.expanded()?"true":"false"),n.expanded()||e.removeClass("details-open")}n.container.find(".customize-section-back").on("click keydown",function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),n.collapse())}),n.headerContainer=J("#accordion-section-"+n.id),n.headerContainer.on("click",".customize-themes-section-title",function(){n.headerContainer.find(".filter-details").length&&(n.headerContainer.find(".customize-themes-section-title").toggleClass("details-open").attr("aria-expanded",function(e,t){return"true"===t?"false":"true"}),n.headerContainer.find(".filter-details").slideToggle(180)),n.expanded()||n.expand()}),n.container.on("click",".theme-actions .preview-theme",function(){Y.panel("themes").loadThemePreview(J(this).data("slug"))}),n.container.on("click",".left",function(){n.previousTheme()}),n.container.on("click",".right",function(){n.nextTheme()}),n.container.on("click",".theme-backdrop, .close",function(){n.closeDetails()}),"local"===n.params.filter_type?n.container.on("input",".wp-filter-search-themes",function(e){n.filterSearch(e.currentTarget.value)}):"remote"===n.params.filter_type&&(e=_.debounce(n.checkTerm,500),n.contentContainer.on("input",".wp-filter-search",function(){Y.panel("themes").expanded()&&(e(n),n.expanded()||n.expand())}),n.contentContainer.on("click",".filter-group input",function(){n.filtersChecked(),n.checkTerm(n)})),n.contentContainer.on("click",".feature-filter-toggle",function(e){var t=J(".customize-themes-full-container"),e=J(e.currentTarget);n.filtersHeight=e.parent().next(".filter-drawer").height(),0<t.scrollTop()&&(t.animate({scrollTop:0},400),e.hasClass("open"))||(e.toggleClass("open").attr("aria-expanded",function(e,t){return"true"===t?"false":"true"}).parent().next(".filter-drawer").slideToggle(180,"linear"),e.hasClass("open")?(t=1018<window.innerWidth?50:76,n.contentContainer.find(".themes").css("margin-top",n.filtersHeight+t)):n.contentContainer.find(".themes").css("margin-top",0))}),n.contentContainer.on("click",".no-themes-local .search-dotorg-themes",function(){Y.section("wporg_themes").focus()}),n.expanded.bind(t),t(),Y.bind("ready",function(){n.contentContainer=n.container.find(".customize-themes-section"),n.contentContainer.appendTo(J(".customize-themes-full-container")),n.container.add(n.headerContainer)})},onChangeExpanded:function(e,n){var i=this,t=i.contentContainer.closest(".customize-themes-full-container");function a(){0===i.loaded&&i.loadThemes(),Y.section.each(function(e){var t;e!==i&&"themes"===e.params.type&&(t=e.contentContainer.find(".wp-filter-search").val(),i.contentContainer.find(".wp-filter-search").val(t),""===t&&""!==i.term&&"local"!==i.params.filter_type?(i.term="",i.initializeNewQuery(i.term,i.tags)):"remote"===i.params.filter_type?i.checkTerm(i):"local"===i.params.filter_type&&i.filterSearch(t),e.collapse({duration:n.duration}))}),i.contentContainer.addClass("current-section"),t.scrollTop(),t.on("scroll",_.throttle(i.renderScreenshots,300)),t.on("scroll",_.throttle(i.loadMore,300)),n.completeCallback&&n.completeCallback(),i.updateCount()}n.unchanged?n.completeCallback&&n.completeCallback():e?i.panel()&&Y.panel.has(i.panel())?Y.panel(i.panel()).expand({duration:n.duration,completeCallback:a}):a():(i.contentContainer.removeClass("current-section"),i.headerContainer.find(".filter-details").slideUp(180),t.off("scroll"),n.completeCallback&&n.completeCallback())},getContent:function(){return this.container.find(".control-section-content")},loadThemes:function(){var n,e,i=this;i.loading||(n=Math.ceil(i.loaded/100)+1,e={nonce:Y.settings.nonce.switch_themes,wp_customize:"on",theme_action:i.params.action,customized_theme:Y.settings.theme.stylesheet,page:n},"remote"===i.params.filter_type&&(e.search=i.term,e.tags=i.tags),i.headContainer.closest(".wp-full-overlay").addClass("loading"),i.loading=!0,i.container.find(".no-themes").hide(),(e=wp.ajax.post("customize_load_themes",e)).done(function(e){var t=e.themes;""!==i.nextTerm||""!==i.nextTags?(i.nextTerm&&(i.term=i.nextTerm),i.nextTags&&(i.tags=i.nextTags),i.nextTerm="",i.nextTags="",i.loading=!1,i.loadThemes()):(0!==t.length?(i.loadControls(t,n),1===n&&(_.each(i.controls().slice(0,3),function(e){e=e.params.theme.screenshot[0];e&&((new Image).src=e)}),"local"!==i.params.filter_type)&&wp.a11y.speak(Y.settings.l10n.themeSearchResults.replace("%d",e.info.results)),_.delay(i.renderScreenshots,100),("local"===i.params.filter_type||t.length<100)&&(i.fullyLoaded=!0)):0===i.loaded?(i.container.find(".no-themes").show(),wp.a11y.speak(i.container.find(".no-themes").text())):i.fullyLoaded=!0,"local"===i.params.filter_type?i.updateCount():i.updateCount(e.info.results),i.container.find(".unexpected-error").hide(),i.headContainer.closest(".wp-full-overlay").removeClass("loading"),i.loading=!1)}),e.fail(function(e){void 0===e?(i.container.find(".unexpected-error").show(),wp.a11y.speak(i.container.find(".unexpected-error").text())):"undefined"!=typeof console&&console.error&&console.error(e),i.headContainer.closest(".wp-full-overlay").removeClass("loading"),i.loading=!1}))},loadControls:function(e,t){var n=[],i=this;_.each(e,function(e){e=new Y.controlConstructor.theme(i.params.action+"_theme_"+e.id,{type:"theme",section:i.params.id,theme:e,priority:i.loaded+1});Y.control.add(e),n.push(e),i.loaded=i.loaded+1}),1!==t&&Array.prototype.push.apply(i.screenshotQueue,n)},loadMore:function(){var e,t;this.fullyLoaded||this.loading||(t=(e=this.container.closest(".customize-themes-full-container")).scrollTop()+e.height(),e.prop("scrollHeight")-3e3<t&&this.loadThemes())},filterSearch:function(e){var t,n=0,i=this,a=Y.section.has("wporg_themes")&&"remote"!==i.params.filter_type?".no-themes-local":".no-themes",o=i.controls();i.loading||(t=e.toLowerCase().trim().replace(/-/g," ").split(" "),_.each(o,function(e){e.filter(t)&&(n+=1)}),0===n?(i.container.find(a).show(),wp.a11y.speak(i.container.find(a).text())):i.container.find(a).hide(),i.renderScreenshots(),Y.reflowPaneContents(),i.updateCountDebounced(n))},checkTerm:function(e){var t;"remote"===e.params.filter_type&&(t=e.contentContainer.find(".wp-filter-search").val(),e.term!==t.trim())&&e.initializeNewQuery(t,e.tags)},filtersChecked:function(){var e=this,t=e.container.find(".filter-group").find(":checkbox"),n=[];_.each(t.filter(":checked"),function(e){n.push(J(e).prop("value"))}),0===n.length?(n="",e.contentContainer.find(".feature-filter-toggle .filter-count-0").show(),e.contentContainer.find(".feature-filter-toggle .filter-count-filters").hide()):(e.contentContainer.find(".feature-filter-toggle .theme-filter-count").text(n.length),e.contentContainer.find(".feature-filter-toggle .filter-count-0").hide(),e.contentContainer.find(".feature-filter-toggle .filter-count-filters").show()),_.isEqual(e.tags,n)||(e.loading?e.nextTags=n:"remote"===e.params.filter_type?e.initializeNewQuery(e.term,n):"local"===e.params.filter_type&&e.filterSearch(n.join(" ")))},initializeNewQuery:function(e,t){var n=this;_.each(n.controls(),function(e){e.container.remove(),Y.control.remove(e.id)}),n.loaded=0,n.fullyLoaded=!1,n.screenshotQueue=null,n.loading?(n.nextTerm=e,n.nextTags=t):(n.term=e,n.tags=t,n.loadThemes()),n.expanded()||n.expand()},renderScreenshots:function(){var o=this;null!==o.screenshotQueue&&0!==o.screenshotQueue.length||(o.screenshotQueue=_.filter(o.controls(),function(e){return!e.screenshotRendered})),o.screenshotQueue.length&&(o.screenshotQueue=_.filter(o.screenshotQueue,function(e){var t,n,i=e.container.find(".theme-screenshot"),a=i.find("img");return!(!a.length||!a.is(":hidden")&&(t=(n=o.$window.scrollTop())+o.$window.height(),a=a.offset().top,(n=n-(i=3*(n=i.height()))<=a+n&&a<=t+i)&&e.container.trigger("render-screenshot"),n))}))},getVisibleCount:function(){return this.contentContainer.find("li.customize-control:visible").length},updateCount:function(e){var t,n;e||0===e||(e=this.getVisibleCount()),n=this.contentContainer.find(".themes-displayed"),t=this.contentContainer.find(".theme-count"),0===e?t.text("0"):(n.fadeOut(180,function(){t.text(e),n.fadeIn(180)}),wp.a11y.speak(Y.settings.l10n.announceThemeCount.replace("%d",e)))},nextTheme:function(){var e=this;e.getNextTheme()&&e.showDetails(e.getNextTheme(),function(){e.overlay.find(".right").focus()})},getNextTheme:function(){var e=Y.control(this.params.action+"_theme_"+this.currentTheme),t=this.controls(),e=_.indexOf(t,e);return-1!==e&&!!(t=t[e+1])&&t.params.theme},previousTheme:function(){var e=this;e.getPreviousTheme()&&e.showDetails(e.getPreviousTheme(),function(){e.overlay.find(".left").focus()})},getPreviousTheme:function(){var e=Y.control(this.params.action+"_theme_"+this.currentTheme),t=this.controls(),e=_.indexOf(t,e);return-1!==e&&!!(t=t[e-1])&&t.params.theme},updateLimits:function(){this.getNextTheme()||this.overlay.find(".right").addClass("disabled"),this.getPreviousTheme()||this.overlay.find(".left").addClass("disabled")},loadThemePreview:function(e){return Y.ThemesPanel.prototype.loadThemePreview.call(this,e)},showDetails:function(e,t){var n=this,i=Y.panel("themes");function a(){return!i.canSwitchTheme(e.id)}n.currentTheme=e.id,n.overlay.html(n.template(e)).fadeIn("fast").focus(),n.overlay.find("button.preview, button.preview-theme").toggleClass("disabled",a()),n.overlay.find("button.theme-install").toggleClass("disabled",a()||!1===Y.settings.theme._canInstall||!0===Y.settings.theme._filesystemCredentialsNeeded),n.$body.addClass("modal-open"),n.containFocus(n.overlay),n.updateLimits(),wp.a11y.speak(Y.settings.l10n.announceThemeDetails.replace("%s",e.name)),t&&t()},closeDetails:function(){this.$body.removeClass("modal-open"),this.overlay.fadeOut("fast"),Y.control(this.params.action+"_theme_"+this.currentTheme).container.find(".theme").focus()},containFocus:function(t){var n;t.on("keydown",function(e){if(9===e.keyCode)return(n=J(":tabbable",t)).last()[0]!==e.target||e.shiftKey?n.first()[0]===e.target&&e.shiftKey?(n.last().focus(),!1):void 0:(n.first().focus(),!1)})}}),Y.OuterSection=Y.Section.extend({initialize:function(){this.containerParent="#customize-outer-theme-controls",this.containerPaneParent=".customize-outer-pane-parent",Y.Section.prototype.initialize.apply(this,arguments)},onChangeExpanded:function(e,t){var n,i=this,a=i.headContainer.closest(".wp-full-overlay-sidebar-content"),o=i.contentContainer,s=o.find(".customize-section-back"),r=i.headContainer.find(".accordion-section-title").first();J(document.body).toggleClass("outer-section-open",e),i.container.toggleClass("open",e),i.container.removeClass("busy"),Y.section.each(function(e){"outer"===e.params.type&&e.id!==i.id&&e.container.removeClass("open")}),e&&!o.hasClass("open")?(n=t.unchanged?t.completeCallback:function(){i._animateChangeExpanded(function(){r.attr("tabindex","-1"),s.attr("tabindex","0"),s.trigger("focus"),o.css("top",""),a.scrollTop(0),t.completeCallback&&t.completeCallback()}),o.addClass("open")}.bind(this),i.panel()?Y.panel(i.panel()).expand({duration:t.duration,completeCallback:n}):n()):!e&&o.hasClass("open")?(i.panel()&&(n=Y.panel(i.panel())).contentContainer.hasClass("skip-transition")&&n.collapse(),i._animateChangeExpanded(function(){s.attr("tabindex","-1"),r.attr("tabindex","0"),r.trigger("focus"),o.css("top",""),t.completeCallback&&t.completeCallback()}),o.removeClass("open")):t.completeCallback&&t.completeCallback()}}),Y.Panel=a.extend({containerType:"panel",initialize:function(e,t){var n=this,i=t.params||t;i.type||_.find(Y.panelConstructor,function(e,t){return e===n.constructor&&(i.type=t,!0)}),a.prototype.initialize.call(n,e,i),n.embed(),n.deferred.embedded.done(function(){n.ready()})},embed:function(){var e=this,t=J("#customize-theme-controls"),n=J(".customize-pane-parent");e.headContainer.parent().is(n)||n.append(e.headContainer),e.contentContainer.parent().is(e.headContainer)||t.append(e.contentContainer),e.renderContent(),e.deferred.embedded.resolve()},attachEvents:function(){var t,n=this;n.headContainer.find(".accordion-section-title").on("click keydown",function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),n.expanded())||n.expand()}),n.container.find(".customize-panel-back").on("click keydown",function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),n.expanded()&&n.collapse())}),(t=n.container.find(".panel-meta:first")).find("> .accordion-section-title .customize-help-toggle").on("click",function(){var e;t.hasClass("cannot-expand")||(e=t.find(".customize-panel-description:first"),t.hasClass("open")?(t.toggleClass("open"),e.slideUp(n.defaultExpandedArguments.duration,function(){e.trigger("toggled")}),J(this).attr("aria-expanded",!1)):(e.slideDown(n.defaultExpandedArguments.duration,function(){e.trigger("toggled")}),t.toggleClass("open"),J(this).attr("aria-expanded",!0)))})},sections:function(){return this._children("panel","section")},isContextuallyActive:function(){var e=this.sections(),t=0;return _(e).each(function(e){e.active()&&e.isContextuallyActive()&&(t+=1)}),0!==t},onChangeExpanded:function(e,t){var n,i,a,o,s,r,c;t.unchanged?t.completeCallback&&t.completeCallback():(a=(i=(n=this).contentContainer).closest(".wp-full-overlay"),o=i.closest(".wp-full-overlay-sidebar-content"),s=n.headContainer.find(".accordion-section-title"),r=i.find(".customize-panel-back"),c=n.sections(),e&&!i.hasClass("current-panel")?(Y.section.each(function(e){n.id!==e.panel()&&e.collapse({duration:0})}),Y.panel.each(function(e){n!==e&&e.collapse({duration:0})}),n.params.autoExpandSoleSection&&1===c.length&&c[0].active.get()?(i.addClass("current-panel skip-transition"),a.addClass("in-sub-panel"),c[0].expand({completeCallback:t.completeCallback})):(n._animateChangeExpanded(function(){s.attr("tabindex","-1"),r.attr("tabindex","0"),r.trigger("focus"),i.css("top",""),o.scrollTop(0),t.completeCallback&&t.completeCallback()}),i.addClass("current-panel"),a.addClass("in-sub-panel")),Y.state("expandedPanel").set(n)):!e&&i.hasClass("current-panel")&&(i.hasClass("skip-transition")?i.removeClass("skip-transition"):n._animateChangeExpanded(function(){s.attr("tabindex","0"),r.attr("tabindex","-1"),s.focus(),i.css("top",""),t.completeCallback&&t.completeCallback()}),a.removeClass("in-sub-panel"),i.removeClass("current-panel"),n===Y.state("expandedPanel").get())&&Y.state("expandedPanel").set(!1))},renderContent:function(){var e=this,t=0!==J("#tmpl-"+e.templateSelector+"-content").length?wp.template(e.templateSelector+"-content"):wp.template("customize-panel-default-content");t&&e.headContainer&&e.contentContainer.html(t(_.extend({id:e.id},e.params)))}}),Y.ThemesPanel=Y.Panel.extend({initialize:function(e,t){this.installingThemes=[],Y.Panel.prototype.initialize.call(this,e,t)},canSwitchTheme:function(e){return!(!e||e!==Y.settings.theme.stylesheet)||"publish"===Y.state("selectedChangesetStatus").get()&&(""===Y.state("changesetStatus").get()||"auto-draft"===Y.state("changesetStatus").get())},attachEvents:function(){var t=this;function e(){t.canSwitchTheme()?t.notifications.remove("theme_switch_unavailable"):t.notifications.add(new Y.Notification("theme_switch_unavailable",{message:Y.l10n.themePreviewUnavailable,type:"warning"}))}Y.Panel.prototype.attachEvents.apply(t),Y.settings.theme._canInstall&&Y.settings.theme._filesystemCredentialsNeeded&&t.notifications.add(new Y.Notification("theme_install_unavailable",{message:Y.l10n.themeInstallUnavailable,type:"info",dismissible:!0})),e(),Y.state("selectedChangesetStatus").bind(e),Y.state("changesetStatus").bind(e),t.contentContainer.on("click",".customize-theme",function(){t.collapse()}),t.contentContainer.on("click",".customize-themes-section-title, .customize-themes-mobile-back",function(){J(".wp-full-overlay").toggleClass("showing-themes")}),t.contentContainer.on("click",".theme-install",function(e){t.installTheme(e)}),t.contentContainer.on("click",".update-theme, #update-theme",function(e){e.preventDefault(),e.stopPropagation(),t.updateTheme(e)}),t.contentContainer.on("click",".delete-theme",function(e){t.deleteTheme(e)}),_.bindAll(t,"installTheme","updateTheme")},onChangeExpanded:function(e,t){var n,i=!1;Y.Panel.prototype.onChangeExpanded.apply(this,[e,t]),t.unchanged?t.completeCallback&&t.completeCallback():(n=this.headContainer.closest(".wp-full-overlay"),e?(n.addClass("in-themes-panel").delay(200).find(".customize-themes-full-container").addClass("animate"),_.delay(function(){n.addClass("themes-panel-expanded")},200),600<window.innerWidth&&(t=this.sections(),_.each(t,function(e){e.expanded()&&(i=!0)}),!i)&&0<t.length&&t[0].expand()):n.removeClass("in-themes-panel themes-panel-expanded").find(".customize-themes-full-container").removeClass("animate"))},installTheme:function(e){var t,i=this,a=J(e.target).data("slug"),o=J.Deferred(),s=J(e.target).hasClass("preview");return Y.settings.theme._filesystemCredentialsNeeded?o.reject({errorCode:"theme_install_unavailable"}):i.canSwitchTheme(a)?_.contains(i.installingThemes,a)?o.reject({errorCode:"theme_already_installing"}):(wp.updates.maybeRequestFilesystemCredentials(e),e=function(t){var e,n=!1;if(s)Y.notifications.remove("theme_installing"),i.loadThemePreview(a);else{if(Y.control.each(function(e){"theme"===e.params.type&&e.params.theme.id===t.slug&&(n=e.params.theme,e.rerenderAsInstalled(!0))}),!n||Y.control.has("installed_theme_"+n.id))return void o.resolve(t);n.type="installed",e=new Y.controlConstructor.theme("installed_theme_"+n.id,{type:"theme",section:"installed_themes",theme:n,priority:0}),Y.control.add(e),Y.control(e.id).container.trigger("render-screenshot"),Y.section.each(function(e){"themes"===e.params.type&&n.id===e.currentTheme&&e.closeDetails()})}o.resolve(t)},i.installingThemes.push(a),t=wp.updates.installTheme({slug:a}),s&&Y.notifications.add(new Y.OverlayNotification("theme_installing",{message:Y.l10n.themeDownloading,type:"info",loading:!0})),t.done(e),t.fail(function(){Y.notifications.remove("theme_installing")})):o.reject({errorCode:"theme_switch_unavailable"}),o.promise()},loadThemePreview:function(e){var t,n,i=J.Deferred();return this.canSwitchTheme(e)?((n=document.createElement("a")).href=location.href,e=_.extend(Y.utils.parseQueryString(n.search.substr(1)),{theme:e,changeset_uuid:Y.settings.changeset.uuid,return:Y.settings.url.return}),Y.state("saved").get()||(e.customize_autosaved="on"),n.search=J.param(e),Y.notifications.add(new Y.OverlayNotification("theme_previewing",{message:Y.l10n.themePreviewWait,type:"info",loading:!0})),t=function(){var e;0<Y.state("processing").get()||(Y.state("processing").unbind(t),(e=Y.requestChangesetUpdate({},{autosave:!0})).done(function(){i.resolve(),J(window).off("beforeunload.customize-confirm"),location.replace(n.href)}),e.fail(function(){Y.notifications.remove("theme_previewing"),i.reject()}))},0===Y.state("processing").get()?t():Y.state("processing").bind(t)):i.reject({errorCode:"theme_switch_unavailable"}),i.promise()},updateTheme:function(e){wp.updates.maybeRequestFilesystemCredentials(e),J(document).one("wp-theme-update-success",function(e,t){Y.control.each(function(e){"theme"===e.params.type&&e.params.theme.id===t.slug&&(e.params.theme.hasUpdate=!1,e.params.theme.version=t.newVersion,setTimeout(function(){e.rerenderAsInstalled(!0)},2e3))})}),wp.updates.updateTheme({slug:J(e.target).closest(".notice").data("slug")})},deleteTheme:function(e){var t=J(e.target).data("slug"),n=Y.section("installed_themes");e.preventDefault(),Y.settings.theme._filesystemCredentialsNeeded||window.confirm(Y.settings.l10n.confirmDeleteTheme)&&(wp.updates.maybeRequestFilesystemCredentials(e),J(document).one("wp-theme-delete-success",function(){var e=Y.control("installed_theme_"+t);e.container.remove(),Y.control.remove(e.id),n.loaded=n.loaded-1,n.updateCount(),Y.control.each(function(e){"theme"===e.params.type&&e.params.theme.id===t&&e.rerenderAsInstalled(!1)})}),wp.updates.deleteTheme({slug:t}),n.closeDetails(),n.focus())}}),Y.Control=Y.Class.extend({defaultActiveArguments:{duration:"fast",completeCallback:J.noop},defaults:{label:"",description:"",active:!0,priority:10},initialize:function(e,t){var n,i=this,a=[];i.params=_.extend({},i.defaults,i.params||{},t.params||t||{}),Y.Control.instanceCounter||(Y.Control.instanceCounter=0),Y.Control.instanceCounter++,i.params.instanceNumber||(i.params.instanceNumber=Y.Control.instanceCounter),i.params.type||_.find(Y.controlConstructor,function(e,t){return e===i.constructor&&(i.params.type=t,!0)}),i.params.content||(i.params.content=J("<li></li>",{id:"customize-control-"+e.replace(/]/g,"").replace(/\[/g,"-"),class:"customize-control customize-control-"+i.params.type})),i.id=e,i.selector="#customize-control-"+e.replace(/\]/g,"").replace(/\[/g,"-"),i.params.content?i.container=J(i.params.content):i.container=J(i.selector),i.params.templateId?i.templateSelector=i.params.templateId:i.templateSelector="customize-control-"+i.params.type+"-content",i.deferred=_.extend(i.deferred||{},{embedded:new J.Deferred}),i.section=new Y.Value,i.priority=new Y.Value,i.active=new Y.Value,i.activeArgumentsQueue=[],i.notifications=new Y.Notifications({alt:i.altNotice}),i.elements=[],i.active.bind(function(e){var t=i.activeArgumentsQueue.shift(),t=J.extend({},i.defaultActiveArguments,t);i.onChangeActive(e,t)}),i.section.set(i.params.section),i.priority.set(isNaN(i.params.priority)?10:i.params.priority),i.active.set(i.params.active),Y.utils.bubbleChildValueChanges(i,["section","priority","active"]),i.settings={},n={},i.params.setting&&(n.default=i.params.setting),_.extend(n,i.params.settings),_.each(n,function(e,t){var n;_.isObject(e)&&_.isFunction(e.extended)&&e.extended(Y.Value)?i.settings[t]=e:_.isString(e)&&((n=Y(e))?i.settings[t]=n:a.push(e))}),t=function(){_.each(n,function(e,t){!i.settings[t]&&_.isString(e)&&(i.settings[t]=Y(e))}),i.settings[0]&&!i.settings.default&&(i.settings.default=i.settings[0]),i.setting=i.settings.default||null,i.linkElements(),i.embed()},0===a.length?t():Y.apply(Y,a.concat(t)),i.deferred.embedded.done(function(){i.linkElements(),i.setupNotifications(),i.ready()})},linkElements:function(){var i,a=this,o=a.container.find("[data-customize-setting-link], [data-customize-setting-key-link]"),s={};o.each(function(){var e,t,n=J(this);if(!n.data("customizeSettingLinked")){if(n.data("customizeSettingLinked",!0),n.is(":radio")){if(e=n.prop("name"),s[e])return;s[e]=!0,n=o.filter('[name="'+e+'"]')}n.data("customizeSettingLink")?t=Y(n.data("customizeSettingLink")):n.data("customizeSettingKeyLink")&&(t=a.settings[n.data("customizeSettingKeyLink")]),t&&(i=new Y.Element(n),a.elements.push(i),i.sync(t),i.set(t()))}})},embed:function(){var n=this,e=function(e){var t;e&&Y.section(e,function(e){e.deferred.embedded.done(function(){t=e.contentContainer.is("ul")?e.contentContainer:e.contentContainer.find("ul:first"),n.container.parent().is(t)||t.append(n.container),n.renderContent(),n.deferred.embedded.resolve()})})};n.section.bind(e),e(n.section.get())},ready:function(){var t,n=this;"dropdown-pages"===n.params.type&&n.params.allow_addition&&((t=n.container.find(".new-content-item")).hide(),n.container.on("click",".add-new-toggle",function(e){J(e.currentTarget).slideUp(180),t.slideDown(180),t.find(".create-item-input").focus()}),n.container.on("click",".add-content",function(){n.addNewPage()}),n.container.on("keydown",".create-item-input",function(e){13===e.which&&n.addNewPage()}))},getNotificationsContainerElement:function(){var e,t=this,n=t.container.find(".customize-control-notifications-container:first");return n.length||(n=J('<div class="customize-control-notifications-container"></div>'),t.container.hasClass("customize-control-nav_menu_item")?t.container.find(".menu-item-settings:first").prepend(n):t.container.hasClass("customize-control-widget_form")?t.container.find(".widget-inside:first").prepend(n):(e=t.container.find(".customize-control-title")).length?e.after(n):t.container.prepend(n)),n},setupNotifications:function(){var n,e,i=this;_.each(i.settings,function(n){n.notifications&&(n.notifications.bind("add",function(e){var t=_.extend({},e,{setting:n.id});i.notifications.add(new Y.Notification(n.id+":"+e.code,t))}),n.notifications.bind("remove",function(e){i.notifications.remove(n.id+":"+e.code)}))}),n=function(){var e=i.section();(!e||Y.section.has(e)&&Y.section(e).expanded())&&i.notifications.render()},i.notifications.bind("rendered",function(){var e=i.notifications.get();i.container.toggleClass("has-notifications",0!==e.length),i.container.toggleClass("has-error",0!==_.where(e,{type:"error"}).length)}),i.section.bind(e=function(e,t){t&&Y.section.has(t)&&Y.section(t).expanded.unbind(n),e&&Y.section(e,function(e){e.expanded.bind(n),n()})}),e(i.section.get()),i.notifications.bind("change",_.debounce(n))},renderNotifications:function(){var e,t,n=this,i=!1;"undefined"!=typeof console&&console.warn&&console.warn("[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method."),(e=n.getNotificationsContainerElement())&&e.length&&(t=[],n.notifications.each(function(e){t.push(e),"error"===e.type&&(i=!0)}),0===t.length?e.stop().slideUp("fast"):e.stop().slideDown("fast",null,function(){J(this).css("height","auto")}),n.notificationsTemplate||(n.notificationsTemplate=wp.template("customize-control-notifications")),n.container.toggleClass("has-notifications",0!==t.length),n.container.toggleClass("has-error",i),e.empty().append(n.notificationsTemplate({notifications:t,altNotice:Boolean(n.altNotice)}).trim()))},expand:function(e){Y.section(this.section()).expand(e)},focus:o,onChangeActive:function(e,t){t.unchanged?t.completeCallback&&t.completeCallback():J.contains(document,this.container[0])?e?this.container.slideDown(t.duration,t.completeCallback):this.container.slideUp(t.duration,t.completeCallback):(this.container.toggle(e),t.completeCallback&&t.completeCallback())},toggle:function(e){return this.onChangeActive(e,this.defaultActiveArguments)},activate:a.prototype.activate,deactivate:a.prototype.deactivate,_toggleActive:a.prototype._toggleActive,dropdownInit:function(){function e(e){"string"==typeof e&&i.statuses&&i.statuses[e]?n.html(i.statuses[e]).show():n.hide()}var t=this,n=this.container.find(".dropdown-status"),i=this.params,a=!1;this.container.on("click keydown",".dropdown",function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),a||t.container.toggleClass("open"),t.container.hasClass("open")&&t.container.parent().parent().find("li.library-selected").focus(),a=!0,setTimeout(function(){a=!1},400))}),this.setting.bind(e),e(this.setting())},renderContent:function(){var e=this,t=e.templateSelector;t==="customize-control-"+e.params.type+"-content"&&_.contains(["button","checkbox","date","datetime-local","email","month","number","password","radio","range","search","select","tel","time","text","textarea","week","url"],e.params.type)&&!document.getElementById("tmpl-"+t)&&0===e.container.children().length&&(t="customize-control-default-content"),document.getElementById("tmpl-"+t)&&(t=wp.template(t))&&e.container&&e.container.html(t(e.params)),e.notifications.container=e.getNotificationsContainerElement(),(!(t=e.section())||Y.section.has(t)&&Y.section(t).expanded())&&e.notifications.render()},addNewPage:function(){var e,a,o,t,s,r,c=this;"dropdown-pages"===c.params.type&&c.params.allow_addition&&Y.Menus&&(a=c.container.find(".add-new-toggle"),o=c.container.find(".new-content-item"),t=c.container.find(".create-item-input"),s=t.val(),r=c.container.find("select"),s?(t.removeClass("invalid"),t.attr("disabled","disabled"),(e=Y.Menus.insertAutoDraftPost({post_title:s,post_type:"page"})).done(function(e){var t,n,i=new Y.Menus.AvailableItemModel({id:"post-"+e.post_id,title:s,type:"post_type",type_label:Y.Menus.data.l10n.page_label,object:"page",object_id:e.post_id,url:e.url});Y.Menus.availableMenuItemsPanel.collection.add(i),t=J("#available-menu-items-post_type-page").find(".available-menu-items-list"),n=wp.template("available-menu-item"),t.prepend(n(i.attributes)),r.focus(),c.setting.set(String(e.post_id)),o.slideUp(180),a.slideDown(180)}),e.always(function(){t.val("").removeAttr("disabled")})):t.addClass("invalid"))}}),Y.ColorControl=Y.Control.extend({ready:function(){var t,n=this,e="hue"===this.params.mode,i=!1;e?(t=this.container.find(".color-picker-hue")).val(n.setting()).wpColorPicker({change:function(e,t){i=!0,n.setting(t.color.h()),i=!1}}):(t=this.container.find(".color-picker-hex")).val(n.setting()).wpColorPicker({change:function(){i=!0,n.setting.set(t.wpColorPicker("color")),i=!1},clear:function(){i=!0,n.setting.set(""),i=!1}}),n.setting.bind(function(e){i||(t.val(e),t.wpColorPicker("color",e))}),n.container.on("keydown",function(e){27===e.which&&n.container.find(".wp-picker-container").hasClass("wp-picker-active")&&(t.wpColorPicker("close"),n.container.find(".wp-color-result").focus(),e.stopPropagation())})}}),Y.MediaControl=Y.Control.extend({ready:function(){var n=this;function e(e){var t=J.Deferred();n.extended(Y.UploadControl)?t.resolve():(e=parseInt(e,10),_.isNaN(e)||e<=0?(delete n.params.attachment,t.resolve()):n.params.attachment&&n.params.attachment.id===e&&t.resolve()),"pending"===t.state()&&wp.media.attachment(e).fetch().done(function(){n.params.attachment=this.attributes,t.resolve(),wp.customize.previewer.send(n.setting.id+"-attachment-data",this.attributes)}),t.done(function(){n.renderContent()})}_.bindAll(n,"restoreDefault","removeFile","openFrame","select","pausePlayer"),n.container.on("click keydown",".upload-button",n.openFrame),n.container.on("click keydown",".upload-button",n.pausePlayer),n.container.on("click keydown",".thumbnail-image img",n.openFrame),n.container.on("click keydown",".default-button",n.restoreDefault),n.container.on("click keydown",".remove-button",n.pausePlayer),n.container.on("click keydown",".remove-button",n.removeFile),n.container.on("click keydown",".remove-button",n.cleanupPlayer),Y.section(n.section()).container.on("expanded",function(){n.player&&n.player.setControlsSize()}).on("collapsed",function(){n.pausePlayer()}),e(n.setting()),n.setting.bind(e)},pausePlayer:function(){this.player&&this.player.pause()},cleanupPlayer:function(){this.player&&wp.media.mixin.removePlayer(this.player)},openFrame:function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),this.frame||this.initFrame(),this.frame.open())},initFrame:function(){this.frame=wp.media({button:{text:this.params.button_labels.frame_button},states:[new wp.media.controller.Library({title:this.params.button_labels.frame_title,library:wp.media.query({type:this.params.mime_type}),multiple:!1,date:!1})]}),this.frame.on("select",this.select)},select:function(){var e=this.frame.state().get("selection").first().toJSON(),t=window._wpmejsSettings||{};this.params.attachment=e,this.setting(e.id),(e=this.container.find("audio, video").get(0))?this.player=new MediaElementPlayer(e,t):this.cleanupPlayer()},restoreDefault:function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),this.params.attachment=this.params.defaultAttachment,this.setting(this.params.defaultAttachment.url))},removeFile:function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),this.params.attachment={},this.setting(""),this.renderContent())}}),Y.UploadControl=Y.MediaControl.extend({select:function(){var e=this.frame.state().get("selection").first().toJSON(),t=window._wpmejsSettings||{};this.params.attachment=e,this.setting(e.url),(e=this.container.find("audio, video").get(0))?this.player=new MediaElementPlayer(e,t):this.cleanupPlayer()},success:function(){},removerVisibility:function(){}}),Y.ImageControl=Y.UploadControl.extend({thumbnailSrc:function(){}}),Y.BackgroundControl=Y.UploadControl.extend({ready:function(){Y.UploadControl.prototype.ready.apply(this,arguments)},select:function(){Y.UploadControl.prototype.select.apply(this,arguments),wp.ajax.post("custom-background-add",{nonce:_wpCustomizeBackground.nonces.add,wp_customize:"on",customize_theme:Y.settings.theme.stylesheet,attachment_id:this.params.attachment.id})}}),Y.BackgroundPositionControl=Y.Control.extend({ready:function(){var e,n=this;n.container.on("change",'input[name="background-position"]',function(){var e=J(this).val().split(" ");n.settings.x(e[0]),n.settings.y(e[1])}),e=_.debounce(function(){var e=n.settings.x.get(),t=n.settings.y.get(),e=String(e)+" "+String(t);n.container.find('input[name="background-position"][value="'+e+'"]').trigger("click")}),n.settings.x.bind(e),n.settings.y.bind(e),e()}}),Y.CroppedImageControl=Y.MediaControl.extend({openFrame:function(e){Y.utils.isKeydownButNotEnterEvent(e)||(this.initFrame(),this.frame.setState("library").open())},initFrame:function(){var e=_wpMediaViewsL10n;this.frame=wp.media({button:{text:e.select,close:!1},states:[new wp.media.controller.Library({title:this.params.button_labels.frame_title,library:wp.media.query({type:"image"}),multiple:!1,date:!1,priority:20,suggestedWidth:this.params.width,suggestedHeight:this.params.height}),new wp.media.controller.CustomizeImageCropper({imgSelectOptions:this.calculateImageSelectOptions,control:this})]}),this.frame.on("select",this.onSelect,this),this.frame.on("cropped",this.onCropped,this),this.frame.on("skippedcrop",this.onSkippedCrop,this)},onSelect:function(){var e=this.frame.state().get("selection").first().toJSON();this.params.width!==e.width||this.params.height!==e.height||this.params.flex_width||this.params.flex_height?this.frame.setState("cropper"):(this.setImageFromAttachment(e),this.frame.close())},onCropped:function(e){this.setImageFromAttachment(e)},calculateImageSelectOptions:function(e,t){var n=t.get("control"),i=!!parseInt(n.params.flex_width,10),a=!!parseInt(n.params.flex_height,10),o=e.get("width"),e=e.get("height"),s=parseInt(n.params.width,10),r=parseInt(n.params.height,10),c=s/r,l=s,d=r;return t.set("canSkipCrop",!n.mustBeCropped(i,a,s,r,o,e)),c<o/e?s=(r=e)*c:r=(s=o)/c,!(c={handles:!0,keys:!0,instance:!0,persistent:!0,imageWidth:o,imageHeight:e,minWidth:s<l?s:l,minHeight:r<d?r:d,x1:t=(o-s)/2,y1:n=(e-r)/2,x2:s+t,y2:r+n})==a&&!1==i&&(c.aspectRatio=s+":"+r),!0==a&&(delete c.minHeight,c.maxWidth=o),!0==i&&(delete c.minWidth,c.maxHeight=e),c},mustBeCropped:function(e,t,n,i,a,o){return(!0!==e||!0!==t)&&!(!0===e&&i===o||!0===t&&n===a||n===a&&i===o||a<=n)},onSkippedCrop:function(){var e=this.frame.state().get("selection").first().toJSON();this.setImageFromAttachment(e)},setImageFromAttachment:function(e){this.params.attachment=e,this.setting(e.id)}}),Y.SiteIconControl=Y.CroppedImageControl.extend({initFrame:function(){var e=_wpMediaViewsL10n;this.frame=wp.media({button:{text:e.select,close:!1},states:[new wp.media.controller.Library({title:this.params.button_labels.frame_title,library:wp.media.query({type:"image"}),multiple:!1,date:!1,priority:20,suggestedWidth:this.params.width,suggestedHeight:this.params.height}),new wp.media.controller.SiteIconCropper({imgSelectOptions:this.calculateImageSelectOptions,control:this})]}),this.frame.on("select",this.onSelect,this),this.frame.on("cropped",this.onCropped,this),this.frame.on("skippedcrop",this.onSkippedCrop,this)},onSelect:function(){var e=this.frame.state().get("selection").first().toJSON(),t=this;this.params.width!==e.width||this.params.height!==e.height||this.params.flex_width||this.params.flex_height?this.frame.setState("cropper"):wp.ajax.post("crop-image",{nonce:e.nonces.edit,id:e.id,context:"site-icon",cropDetails:{x1:0,y1:0,width:this.params.width,height:this.params.height,dst_width:this.params.width,dst_height:this.params.height}}).done(function(e){t.setImageFromAttachment(e),t.frame.close()}).fail(function(){t.frame.trigger("content:error:crop")})},setImageFromAttachment:function(t){var n;_.each(["site_icon-32","thumbnail","full"],function(e){n||_.isUndefined(t.sizes[e])||(n=t.sizes[e])}),this.params.attachment=t,this.setting(t.id),n&&J('link[rel="icon"][sizes="32x32"]').attr("href",n.url)},removeFile:function(e){Y.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),this.params.attachment={},this.setting(""),this.renderContent(),J('link[rel="icon"][sizes="32x32"]').attr("href","/favicon.ico"))}}),Y.HeaderControl=Y.Control.extend({ready:function(){this.btnRemove=J("#customize-control-header_image .actions .remove"),this.btnNew=J("#customize-control-header_image .actions .new"),_.bindAll(this,"openMedia","removeImage"),this.btnNew.on("click",this.openMedia),this.btnRemove.on("click",this.removeImage),Y.HeaderTool.currentHeader=this.getInitialHeaderImage(),new Y.HeaderTool.CurrentView({model:Y.HeaderTool.currentHeader,el:"#customize-control-header_image .current .container"}),new Y.HeaderTool.ChoiceListView({collection:Y.HeaderTool.UploadsList=new Y.HeaderTool.ChoiceList,el:"#customize-control-header_image .choices .uploaded .list"}),new Y.HeaderTool.ChoiceListView({collection:Y.HeaderTool.DefaultsList=new Y.HeaderTool.DefaultsList,el:"#customize-control-header_image .choices .default .list"}),Y.HeaderTool.combinedList=Y.HeaderTool.CombinedList=new Y.HeaderTool.CombinedList([Y.HeaderTool.UploadsList,Y.HeaderTool.DefaultsList]),wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize="on",wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme=Y.settings.theme.stylesheet},getInitialHeaderImage:function(){var e;return Y.get().header_image&&Y.get().header_image_data&&!_.contains(["remove-header","random-default-image","random-uploaded-image"],Y.get().header_image)?(e=(e=_.find(_wpCustomizeHeader.uploads,function(e){return e.attachment_id===Y.get().header_image_data.attachment_id}))||{url:Y.get().header_image,thumbnail_url:Y.get().header_image,attachment_id:Y.get().header_image_data.attachment_id},new Y.HeaderTool.ImageModel({header:e,choice:e.url.split("/").pop()})):new Y.HeaderTool.ImageModel},calculateImageSelectOptions:function(e,t){var n=parseInt(_wpCustomizeHeader.data.width,10),i=parseInt(_wpCustomizeHeader.data.height,10),a=!!parseInt(_wpCustomizeHeader.data["flex-width"],10),o=!!parseInt(_wpCustomizeHeader.data["flex-height"],10),s=e.get("width"),e=e.get("height");return this.headerImage=new Y.HeaderTool.ImageModel,this.headerImage.set({themeWidth:n,themeHeight:i,themeFlexWidth:a,themeFlexHeight:o,imageWidth:s,imageHeight:e}),t.set("canSkipCrop",!this.headerImage.shouldBeCropped()),(t=n/i)<s/e?n=(i=e)*t:i=(n=s)/t,!(t={handles:!0,keys:!0,instance:!0,persistent:!0,imageWidth:s,imageHeight:e,x1:0,y1:0,x2:n,y2:i})==o&&!1==a&&(t.aspectRatio=n+":"+i),!1==o&&(t.maxHeight=i),!1==a&&(t.maxWidth=n),t},openMedia:function(e){var t=_wpMediaViewsL10n;e.preventDefault(),this.frame=wp.media({button:{text:t.selectAndCrop,close:!1},states:[new wp.media.controller.Library({title:t.chooseImage,library:wp.media.query({type:"image"}),multiple:!1,date:!1,priority:20,suggestedWidth:_wpCustomizeHeader.data.width,suggestedHeight:_wpCustomizeHeader.data.height}),new wp.media.controller.Cropper({imgSelectOptions:this.calculateImageSelectOptions})]}),this.frame.on("select",this.onSelect,this),this.frame.on("cropped",this.onCropped,this),this.frame.on("skippedcrop",this.onSkippedCrop,this),this.frame.open()},onSelect:function(){this.frame.setState("cropper")},onCropped:function(e){var t=e.url,n=e.attachment_id,i=e.width,e=e.height;this.setImageFromURL(t,n,i,e)},onSkippedCrop:function(e){var t=e.get("url"),n=e.get("width"),i=e.get("height");this.setImageFromURL(t,e.id,n,i)},setImageFromURL:function(e,t,n,i){var a={};a.url=e,a.thumbnail_url=e,a.timestamp=_.now(),t&&(a.attachment_id=t),n&&(a.width=n),i&&(a.height=i),t=new Y.HeaderTool.ImageModel({header:a,choice:e.split("/").pop()}),Y.HeaderTool.UploadsList.add(t),Y.HeaderTool.currentHeader.set(t.toJSON()),t.save(),t.importImage()},removeImage:function(){Y.HeaderTool.currentHeader.trigger("hide"),Y.HeaderTool.CombinedList.trigger("control:removeImage")}}),Y.ThemeControl=Y.Control.extend({touchDrag:!1,screenshotRendered:!1,ready:function(){var n=this,e=Y.panel("themes");function t(){return!e.canSwitchTheme(n.params.theme.id)}function i(){n.container.find("button.preview, button.preview-theme").toggleClass("disabled",t()),n.container.find("button.theme-install").toggleClass("disabled",t()||!1===Y.settings.theme._canInstall||!0===Y.settings.theme._filesystemCredentialsNeeded)}Y.state("selectedChangesetStatus").bind(i),Y.state("changesetStatus").bind(i),i(),n.container.on("touchmove",".theme",function(){n.touchDrag=!0}),n.container.on("click keydown touchend",".theme",function(e){var t;if(!Y.utils.isKeydownButNotEnterEvent(e))return!0===n.touchDrag?n.touchDrag=!1:void(J(e.target).is(".theme-actions .button, .update-theme")||(e.preventDefault(),(t=Y.section(n.section())).showDetails(n.params.theme,function(){Y.settings.theme._filesystemCredentialsNeeded&&t.overlay.find(".theme-actions .delete-theme").remove()})))}),n.container.on("render-screenshot",function(){var e=J(this).find("img"),t=e.data("src");t&&e.attr("src",t),n.screenshotRendered=!0})},filter:function(e){var t=this,n=0,i=(i=t.params.theme.name+" "+t.params.theme.description+" "+t.params.theme.tags+" "+t.params.theme.author+" ").toLowerCase().replace("-"," ");return _.isArray(e)||(e=[e]),t.params.theme.name.toLowerCase()===e.join(" ")?n=100:(n+=10*(i.split(e.join(" ")).length-1),_.each(e,function(e){n=(n+=2*(i.split(e+" ").length-1))+i.split(e).length-1}),99<n&&(n=99)),0!==n?(t.activate(),t.params.priority=101-n,!0):(t.deactivate(),!(t.params.priority=101))},rerenderAsInstalled:function(e){var t=this;e?t.params.theme.type="installed":(e=Y.section(t.params.section),t.params.theme.type=e.params.action),t.renderContent(),t.container.trigger("render-screenshot")}}),Y.CodeEditorControl=Y.Control.extend({initialize:function(e,t){var n=this;n.deferred=_.extend(n.deferred||{},{codemirror:J.Deferred()}),Y.Control.prototype.initialize.call(n,e,t),n.notifications.bind("add",function(e){var t;e.code===n.setting.id+":csslint_error"&&(e.templateId="customize-code-editor-lint-error-notification",e.render=(t=e.render,function(){var e=t.call(this);return e.find("input[type=checkbox]").on("click",function(){n.setting.notifications.remove("csslint_error")}),e}))})},ready:function(){var i=this;i.section()?Y.section(i.section(),function(n){n.deferred.embedded.done(function(){var t;n.expanded()?i.initEditor():n.expanded.bind(t=function(e){e&&(i.initEditor(),n.expanded.unbind(t))})})}):i.initEditor()},initEditor:function(){var e,t=this,n=!1;wp.codeEditor&&(_.isUndefined(t.params.editor_settings)||!1!==t.params.editor_settings)&&((n=wp.codeEditor.defaultSettings?_.clone(wp.codeEditor.defaultSettings):{}).codemirror=_.extend({},n.codemirror,{indentUnit:2,tabSize:2}),_.isObject(t.params.editor_settings))&&_.each(t.params.editor_settings,function(e,t){_.isObject(e)&&(n[t]=_.extend({},n[t],e))}),e=new Y.Element(t.container.find("textarea")),t.elements.push(e),e.sync(t.setting),e.set(t.setting()),n?t.initSyntaxHighlightingEditor(n):t.initPlainTextareaEditor()},focus:function(e){var t=this,e=_.extend({},e),n=e.completeCallback;e.completeCallback=function(){n&&n(),t.editor&&t.editor.codemirror.focus()},Y.Control.prototype.focus.call(t,e)},initSyntaxHighlightingEditor:function(e){var t=this,n=t.container.find("textarea"),i=!1,e=_.extend({},e,{onTabNext:_.bind(t.onTabNext,t),onTabPrevious:_.bind(t.onTabPrevious,t),onUpdateErrorNotice:_.bind(t.onUpdateErrorNotice,t)});t.editor=wp.codeEditor.initialize(n,e),J(t.editor.codemirror.display.lineDiv).attr({role:"textbox","aria-multiline":"true","aria-label":t.params.label,"aria-describedby":"editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"}),t.container.find("label").on("click",function(){t.editor.codemirror.focus()}),t.editor.codemirror.on("change",function(e){i=!0,n.val(e.getValue()).trigger("change"),i=!1}),t.setting.bind(function(e){i||t.editor.codemirror.setValue(e)}),t.editor.codemirror.on("keydown",function(e,t){27===t.keyCode&&t.stopPropagation()}),t.deferred.codemirror.resolveWith(t,[t.editor.codemirror])},onTabNext:function(){var e=Y.section(this.section()).controls(),t=e.indexOf(this);e.length===t+1?J("#customize-footer-actions .collapse-sidebar").trigger("focus"):e[t+1].container.find(":focusable:first").focus()},onTabPrevious:function(){var e=Y.section(this.section()),t=e.controls(),n=t.indexOf(this);(0===n?e.contentContainer.find(".customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close").last():t[n-1].contentContainer.find(":focusable:first")).focus()},onUpdateErrorNotice:function(e){this.setting.notifications.remove("csslint_error"),0!==e.length&&(e=1===e.length?Y.l10n.customCssError.singular.replace("%d","1"):Y.l10n.customCssError.plural.replace("%d",String(e.length)),this.setting.notifications.add(new Y.Notification("csslint_error",{message:e,type:"error"})))},initPlainTextareaEditor:function(){var a=this.container.find("textarea"),o=a[0];a.on("blur",function(){a.data("next-tab-blurs",!1)}),a.on("keydown",function(e){var t,n,i;27===e.keyCode?a.data("next-tab-blurs")||(a.data("next-tab-blurs",!0),e.stopPropagation()):9!==e.keyCode||e.ctrlKey||e.altKey||e.shiftKey||a.data("next-tab-blurs")||(t=o.selectionStart,n=o.selectionEnd,i=o.value,0<=t&&(o.value=i.substring(0,t).concat("\t",i.substring(n)),a.selectionStart=o.selectionEnd=t+1),e.stopPropagation(),e.preventDefault())}),this.deferred.codemirror.rejectWith(this)}}),Y.DateTimeControl=Y.Control.extend({ready:function(){var i=this;if(i.inputElements={},i.invalidDate=!1,_.bindAll(i,"populateSetting","updateDaysForMonth","populateDateInputs"),!i.setting)throw new Error("Missing setting");i.container.find(".date-input").each(function(){var e=J(this),t=e.data("component"),n=new Y.Element(e);i.inputElements[t]=n,i.elements.push(n),e.on("change",function(){i.invalidDate&&i.notifications.add(new Y.Notification("invalid_date",{message:Y.l10n.invalidDate}))}),e.on("input",_.debounce(function(){i.invalidDate||i.notifications.remove("invalid_date")})),e.on("blur",_.debounce(function(){i.invalidDate||i.populateDateInputs()}))}),i.inputElements.month.bind(i.updateDaysForMonth),i.inputElements.year.bind(i.updateDaysForMonth),i.populateDateInputs(),i.setting.bind(i.populateDateInputs),_.each(i.inputElements,function(e){e.bind(i.populateSetting)})},parseDateTime:function(e){var t;return(t=e?e.match(/^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/):t)?(t.shift(),e={year:t.shift(),month:t.shift(),day:t.shift(),hour:t.shift()||"00",minute:t.shift()||"00",second:t.shift()||"00"},this.params.includeTime&&this.params.twelveHourFormat&&(e.hour=parseInt(e.hour,10),e.meridian=12<=e.hour?"pm":"am",e.hour=e.hour%12?String(e.hour%12):String(12),delete e.second),e):null},validateInputs:function(){var e,i,a=this;return a.invalidDate=!1,e=["year","day"],a.params.includeTime&&e.push("hour","minute"),_.find(e,function(e){var t,n,e=a.inputElements[e];return i=e.element.get(0),t=parseInt(e.element.attr("max"),10),n=parseInt(e.element.attr("min"),10),e=parseInt(e(),10),a.invalidDate=isNaN(e)||t<e||e<n,a.invalidDate||i.setCustomValidity(""),a.invalidDate}),a.inputElements.meridian&&!a.invalidDate&&(i=a.inputElements.meridian.element.get(0),"am"!==a.inputElements.meridian.get()&&"pm"!==a.inputElements.meridian.get()?a.invalidDate=!0:i.setCustomValidity("")),a.invalidDate?i.setCustomValidity(Y.l10n.invalidValue):i.setCustomValidity(""),(!a.section()||Y.section.has(a.section())&&Y.section(a.section()).expanded())&&_.result(i,"reportValidity"),a.invalidDate},updateDaysForMonth:function(){var e=this,t=parseInt(e.inputElements.month(),10),n=parseInt(e.inputElements.year(),10),i=parseInt(e.inputElements.day(),10);t&&n&&(n=new Date(n,t,0).getDate(),e.inputElements.day.element.attr("max",n),n<i)&&e.inputElements.day(String(n))},populateSetting:function(){var e,t=this;return!(t.validateInputs()||!t.params.allowPastDate&&!t.isFutureDate()||(e=t.convertInputDateToString(),t.setting.set(e),0))},convertInputDateToString:function(){var e,n=this,t="",i=function(e,t){return String(e).length<t&&(t=t-String(e).length,e=Math.pow(10,t).toString().substr(1)+String(e)),e},a=function(e){var t=parseInt(n.inputElements[e].get(),10);return _.contains(["month","day","hour","minute"],e)?t=i(t,2):"year"===e&&(t=i(t,4)),t},o=["year","-","month","-","day"];return n.params.includeTime&&(e=n.inputElements.meridian?n.convertHourToTwentyFourHourFormat(n.inputElements.hour(),n.inputElements.meridian()):n.inputElements.hour(),o=o.concat([" ",i(e,2),":","minute",":","00"])),_.each(o,function(e){t+=n.inputElements[e]?a(e):e}),t},isFutureDate:function(){return 0<Y.utils.getRemainingTime(this.convertInputDateToString())},convertHourToTwentyFourHourFormat:function(e,t){e=parseInt(e,10);return isNaN(e)?"":(t="pm"===t&&e<12?e+12:"am"===t&&12===e?e-12:e,String(t))},populateDateInputs:function(){var i=this.parseDateTime(this.setting.get());return!!i&&(_.each(this.inputElements,function(e,t){var n=i[t];"month"===t||"meridian"===t?(n=n.replace(/^0/,""),e.set(n)):(n=parseInt(n,10),e.element.is(document.activeElement)?n!==parseInt(e(),10)&&e.set(String(n)):e.set(i[t]))}),!0)},toggleFutureDateNotification:function(e){var t="not_future_date";return e?(e=new Y.Notification(t,{type:"error",message:Y.l10n.futureDateError}),this.notifications.add(e)):this.notifications.remove(t),this}}),Y.PreviewLinkControl=Y.Control.extend({defaults:_.extend({},Y.Control.prototype.defaults,{templateId:"customize-preview-link-control"}),ready:function(){var e,t,n,i,a,o=this;_.bindAll(o,"updatePreviewLink"),o.setting||(o.setting=new Y.Value),o.previewElements={},o.container.find(".preview-control-element").each(function(){t=J(this),e=t.data("component"),t=new Y.Element(t),o.previewElements[e]=t,o.elements.push(t)}),n=o.previewElements.url,i=o.previewElements.input,a=o.previewElements.button,i.link(o.setting),n.link(o.setting),n.bind(function(e){n.element.parent().attr({href:e,target:Y.settings.changeset.uuid})}),Y.bind("ready",o.updatePreviewLink),Y.state("saved").bind(o.updatePreviewLink),Y.state("changesetStatus").bind(o.updatePreviewLink),Y.state("activated").bind(o.updatePreviewLink),Y.previewer.previewUrl.bind(o.updatePreviewLink),a.element.on("click",function(e){e.preventDefault(),o.setting()&&(i.element.select(),document.execCommand("copy"),a(a.element.data("copied-text")))}),n.element.parent().on("click",function(e){J(this).hasClass("disabled")&&e.preventDefault()}),a.element.on("mouseenter",function(){o.setting()&&a(a.element.data("copy-text"))})},updatePreviewLink:function(){var e=!Y.state("saved").get()||""===Y.state("changesetStatus").get()||"auto-draft"===Y.state("changesetStatus").get();this.toggleSaveNotification(e),this.previewElements.url.element.parent().toggleClass("disabled",e),this.previewElements.button.element.prop("disabled",e),this.setting.set(Y.previewer.getFrontendPreviewUrl())},toggleSaveNotification:function(e){var t="changes_not_saved";e?(e=new Y.Notification(t,{type:"info",message:Y.l10n.saveBeforeShare}),this.notifications.add(e)):this.notifications.remove(t)}}),Y.defaultConstructor=Y.Setting,Y.control=new Y.Values({defaultConstructor:Y.Control}),Y.section=new Y.Values({defaultConstructor:Y.Section}),Y.panel=new Y.Values({defaultConstructor:Y.Panel}),Y.notifications=new Y.Notifications,Y.PreviewFrame=Y.Messenger.extend({sensitivity:null,initialize:function(e,t){var n=J.Deferred();n.promise(this),this.container=e.container,J.extend(e,{channel:Y.PreviewFrame.uuid()}),Y.Messenger.prototype.initialize.call(this,e,t),this.add("previewUrl",e.previewUrl),this.query=J.extend(e.query||{},{customize_messenger_channel:this.channel()}),this.run(n)},run:function(t){var e,n,i,a=this,o=!1,s=!1,r=null,c="{}"!==a.query.customized;a._ready&&a.unbind("ready",a._ready),a._ready=function(e){s=!0,r=e,a.container.addClass("iframe-ready"),e&&o&&t.resolveWith(a,[e])},a.bind("ready",a._ready),(e=document.createElement("a")).href=a.previewUrl(),n=_.extend(Y.utils.parseQueryString(e.search.substr(1)),{customize_changeset_uuid:a.query.customize_changeset_uuid,customize_theme:a.query.customize_theme,customize_messenger_channel:a.query.customize_messenger_channel}),!Y.settings.changeset.autosaved&&Y.state("saved").get()||(n.customize_autosaved="on"),e.search=J.param(n),a.iframe=J("<iframe />",{title:Y.l10n.previewIframeTitle,name:"customize-"+a.channel()}),a.iframe.attr("onmousewheel",""),a.iframe.attr("sandbox","allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"),c?a.iframe.attr("data-src",e.href):a.iframe.attr("src",e.href),a.iframe.appendTo(a.container),a.targetWindow(a.iframe[0].contentWindow),c&&((i=J("<form>",{action:e.href,target:a.iframe.attr("name"),method:"post",hidden:"hidden"})).append(J("<input>",{type:"hidden",name:"_method",value:"GET"})),_.each(a.query,function(e,t){i.append(J("<input>",{type:"hidden",name:t,value:e}))}),a.container.append(i),i.trigger("submit"),i.remove()),a.bind("iframe-loading-error",function(e){a.iframe.remove(),0===e?a.login(t):-1===e?t.rejectWith(a,["cheatin"]):t.rejectWith(a,["request failure"])}),a.iframe.one("load",function(){o=!0,s?t.resolveWith(a,[r]):setTimeout(function(){t.rejectWith(a,["ready timeout"])},a.sensitivity)})},login:function(n){var i=this,a=function(){n.rejectWith(i,["logged out"])};if(this.triedLogin)return a();J.get(Y.settings.url.ajax,{action:"logged-in"}).fail(a).done(function(e){var t;"1"!==e&&a(),(t=J("<iframe />",{src:i.previewUrl(),title:Y.l10n.previewIframeTitle}).hide()).appendTo(i.container),t.on("load",function(){i.triedLogin=!0,t.remove(),i.run(n)})})},destroy:function(){Y.Messenger.prototype.destroy.call(this),this.iframe&&this.iframe.remove(),delete this.iframe,delete this.targetWindow}}),i=0,Y.PreviewFrame.uuid=function(){return"preview-"+String(i++)},Y.setDocumentTitle=function(e){e=Y.settings.documentTitleTmpl.replace("%s",e);document.title=e,Y.trigger("title",e)},Y.Previewer=Y.Messenger.extend({refreshBuffer:null,initialize:function(e,t){var n,o=this,i=document.createElement("a");J.extend(o,t||{}),o.deferred={active:J.Deferred()},o.refresh=_.debounce((n=o.refresh,function(){var e,t=function(){return 0===Y.state("processing").get()};t()?n.call(o):(e=function(){t()&&(n.call(o),Y.state("processing").unbind(e))},Y.state("processing").bind(e))}),o.refreshBuffer),o.container=Y.ensure(e.container),o.allowedUrls=e.allowedUrls,e.url=window.location.href,Y.Messenger.prototype.initialize.call(o,e),i.href=o.origin(),o.add("scheme",i.protocol.replace(/:$/,"")),o.add("previewUrl",e.previewUrl).setter(function(e){var n,i=null,t=[],a=document.createElement("a");return a.href=e,/\/wp-(admin|includes|content)(\/|$)/.test(a.pathname)?null:(1<a.search.length&&(delete(e=Y.utils.parseQueryString(a.search.substr(1))).customize_changeset_uuid,delete e.customize_theme,delete e.customize_messenger_channel,delete e.customize_autosaved,_.isEmpty(e)?a.search="":a.search=J.param(e)),t.push(a),o.scheme.get()+":"!==a.protocol&&((a=document.createElement("a")).href=t[0].href,a.protocol=o.scheme.get()+":",t.unshift(a)),n=document.createElement("a"),_.find(t,function(t){return!_.isUndefined(_.find(o.allowedUrls,function(e){if(n.href=e,a.protocol===n.protocol&&a.host===n.host&&0===a.pathname.indexOf(n.pathname.replace(/\/$/,"")))return i=t.href,!0}))}),i)}),o.bind("ready",o.ready),o.deferred.active.done(_.bind(o.keepPreviewAlive,o)),o.bind("synced",function(){o.send("active")}),o.previewUrl.bind(o.refresh),o.scroll=0,o.bind("scroll",function(e){o.scroll=e}),o.bind("url",function(e){var t,n=!1;o.scroll=0,o.previewUrl.bind(t=function(){n=!0}),o.previewUrl.set(e),o.previewUrl.unbind(t),n||o.refresh()}),o.bind("documentTitle",function(e){Y.setDocumentTitle(e)})},ready:function(e){var t=this,n={};n.settings=Y.get(),n["settings-modified-while-loading"]=t.settingsModifiedWhileLoading,"resolved"===t.deferred.active.state()&&!t.loading||(n.scroll=t.scroll),n["edit-shortcut-visibility"]=Y.state("editShortcutVisibility").get(),t.send("sync",n),e.currentUrl&&(t.previewUrl.unbind(t.refresh),t.previewUrl.set(e.currentUrl),t.previewUrl.bind(t.refresh)),n={panel:e.activePanels,section:e.activeSections,control:e.activeControls},_(n).each(function(n,i){Y[i].each(function(e,t){_.isUndefined(Y.settings[i+"s"][t])&&_.isUndefined(n[t])||(n[t]?e.activate():e.deactivate())})}),e.settingValidities&&Y._handleSettingValidities({settingValidities:e.settingValidities,focusInvalidControl:!1})},keepPreviewAlive:function(){var e,t=function(){e=setTimeout(i,Y.settings.timeouts.keepAliveCheck)},n=function(){Y.state("previewerAlive").set(!0),clearTimeout(e),t()},i=function(){Y.state("previewerAlive").set(!1)};t(),this.bind("ready",n),this.bind("keep-alive",n)},query:function(){},abort:function(){this.loading&&(this.loading.destroy(),delete this.loading)},refresh:function(){var e,i=this;i.send("loading-initiated"),i.abort(),i.loading=new Y.PreviewFrame({url:i.url(),previewUrl:i.previewUrl(),query:i.query({excludeCustomizedSaved:!0})||{},container:i.container}),i.settingsModifiedWhileLoading={},Y.bind("change",e=function(e){i.settingsModifiedWhileLoading[e.id]=!0}),i.loading.always(function(){Y.unbind("change",e)}),i.loading.done(function(e){var t,n=this;i.preview=n,i.targetWindow(n.targetWindow()),i.channel(n.channel()),t=function(){n.unbind("synced",t),i._previousPreview&&i._previousPreview.destroy(),i._previousPreview=i.preview,i.deferred.active.resolve(),delete i.loading},n.bind("synced",t),i.trigger("ready",e)}),i.loading.fail(function(e){i.send("loading-failed"),"logged out"===e&&(i.preview&&(i.preview.destroy(),delete i.preview),i.login().done(i.refresh)),"cheatin"===e&&i.cheatin()})},login:function(){var t,n,i,a=this;return this._login||(t=J.Deferred(),this._login=t.promise(),n=new Y.Messenger({channel:"login",url:Y.settings.url.login}),i=J("<iframe />",{src:Y.settings.url.login,title:Y.l10n.loginIframeTitle}).appendTo(this.container),n.targetWindow(i[0].contentWindow),n.bind("login",function(){var e=a.refreshNonces();e.always(function(){i.remove(),n.destroy(),delete a._login}),e.done(function(){t.resolve()}),e.fail(function(){a.cheatin(),t.reject()})})),this._login},cheatin:function(){J(document.body).empty().addClass("cheatin").append("<h1>"+Y.l10n.notAllowedHeading+"</h1><p>"+Y.l10n.notAllowed+"</p>")},refreshNonces:function(){var e,t=J.Deferred();return t.promise(),(e=wp.ajax.post("customize_refresh_nonces",{wp_customize:"on",customize_theme:Y.settings.theme.stylesheet})).done(function(e){Y.trigger("nonce-refresh",e),t.resolve()}),e.fail(function(){t.reject()}),t}}),Y.settingConstructor={},Y.controlConstructor={color:Y.ColorControl,media:Y.MediaControl,upload:Y.UploadControl,image:Y.ImageControl,cropped_image:Y.CroppedImageControl,site_icon:Y.SiteIconControl,header:Y.HeaderControl,background:Y.BackgroundControl,background_position:Y.BackgroundPositionControl,theme:Y.ThemeControl,date_time:Y.DateTimeControl,code_editor:Y.CodeEditorControl},Y.panelConstructor={themes:Y.ThemesPanel},Y.sectionConstructor={themes:Y.ThemesSection,outer:Y.OuterSection},Y._handleSettingValidities=function(e){var o=[],n=!1;_.each(e.settingValidities,function(t,e){var a=Y(e);a&&(_.isObject(t)&&_.each(t,function(e,t){var n=!1,e=new Y.Notification(t,_.extend({fromServer:!0},e)),i=a.notifications(e.code);(n=i?e.type!==i.type||e.message!==i.message||!_.isEqual(e.data,i.data):n)&&a.notifications.remove(t),a.notifications.has(e.code)||a.notifications.add(e),o.push(a.id)}),a.notifications.each(function(e){!e.fromServer||"error"!==e.type||!0!==t&&t[e.code]||a.notifications.remove(e.code)}))}),e.focusInvalidControl&&(e=Y.findControlsForSettings(o),_(_.values(e)).find(function(e){return _(e).find(function(e){var t=e.section()&&Y.section.has(e.section())&&Y.section(e.section()).expanded();return(t=t&&e.expanded?e.expanded():t)&&(e.focus(),n=!0),n})}),n||_.isEmpty(e)||_.values(e)[0][0].focus())},Y.findControlsForSettings=function(e){var n,i={};return _.each(_.unique(e),function(e){var t=Y(e);t&&(n=t.findControls())&&0<n.length&&(i[e]=n)}),i},Y.reflowPaneContents=_.bind(function(){var i,e,t,a=[],o=!1;document.activeElement&&(e=J(document.activeElement)),Y.panel.each(function(e){var t,n;"themes"===e.id||(t=e.sections(),n=_.pluck(t,"headContainer"),a.push(e),i=e.contentContainer.is("ul")?e.contentContainer:e.contentContainer.find("ul:first"),Y.utils.areElementListsEqual(n,i.children("[id]")))||(_(t).each(function(e){i.append(e.headContainer)}),o=!0)}),Y.section.each(function(e){var t=e.controls(),n=_.pluck(t,"container");e.panel()||a.push(e),i=e.contentContainer.is("ul")?e.contentContainer:e.contentContainer.find("ul:first"),Y.utils.areElementListsEqual(n,i.children("[id]"))||(_(t).each(function(e){i.append(e.container)}),o=!0)}),a.sort(Y.utils.prioritySort),t=_.pluck(a,"headContainer"),i=J("#customize-theme-controls .customize-pane-parent"),Y.utils.areElementListsEqual(t,i.children())||(_(a).each(function(e){i.append(e.headContainer)}),o=!0),Y.panel.each(function(e){var t=e.active();e.active.callbacks.fireWith(e.active,[t,t])}),Y.section.each(function(e){var t=e.active();e.active.callbacks.fireWith(e.active,[t,t])}),o&&e&&e.trigger("focus"),Y.trigger("pane-contents-reflowed")},Y),Y.state=new Y.Values,_.each(["saved","saving","trashing","activated","processing","paneVisible","expandedPanel","expandedSection","changesetDate","selectedChangesetDate","changesetStatus","selectedChangesetStatus","remainingTimeToPublish","previewerAlive","editShortcutVisibility","changesetLocked","previewedDevice"],function(e){Y.state.create(e)}),J(function(){var h,o,t,n,i,d,u,p,a,s,r,c,l,f,m,H,L,g,v,w,b,M,O,C,j,y,e,x,k,z,S,T,E,R,B,W,D,N,P,I,U,A;function F(e){e&&e.lockUser&&(Y.settings.changeset.lockUser=e.lockUser),Y.state("changesetLocked").set(!0),Y.notifications.add(new j("changeset_locked",{lockUser:Y.settings.changeset.lockUser,allowOverride:Boolean(e&&e.allowOverride)}))}function q(){var e,t=document.createElement("a");return t.href=location.href,e=Y.utils.parseQueryString(t.search.substr(1)),Y.settings.changeset.latestAutoDraftUuid?e.changeset_uuid=Y.settings.changeset.latestAutoDraftUuid:e.customize_autosaved="on",e.return=Y.settings.url.return,t.search=J.param(e),t.href}function Q(){T||(wp.ajax.post("customize_dismiss_autosave_or_lock",{wp_customize:"on",customize_theme:Y.settings.theme.stylesheet,customize_changeset_uuid:Y.settings.changeset.uuid,nonce:Y.settings.nonce.dismiss_autosave_or_lock,dismiss_autosave:!0}),T=!0)}function K(){var e;return Y.state("activated").get()?(""!==(e=Y.state("changesetStatus").get())&&"auto-draft"!==e||(e="publish"),Y.state("selectedChangesetStatus").get()===e&&("future"!==Y.state("selectedChangesetStatus").get()||Y.state("selectedChangesetDate").get()===Y.state("changesetDate").get())&&Y.state("saved").get()&&"auto-draft"!==Y.state("changesetStatus").get()):0===Y._latestRevision}function V(){Y.unbind("change",V),Y.state("selectedChangesetStatus").unbind(V),Y.state("selectedChangesetDate").unbind(V),J(window).on("beforeunload.customize-confirm",function(){if(!K()&&!Y.state("changesetLocked").get())return setTimeout(function(){t.removeClass("customize-loading")},1),Y.l10n.saveAlert})}function $(){var e=J.Deferred(),t=!1,n=!1;return K()?n=!0:confirm(Y.l10n.saveAlert)?(n=!0,Y.each(function(e){e._dirty=!1}),J(document).off("visibilitychange.wp-customize-changeset-update"),J(window).off("beforeunload.wp-customize-changeset-update"),i.css("cursor","progress"),""!==Y.state("changesetStatus").get()&&(t=!0)):e.reject(),(n||t)&&wp.ajax.send("customize_dismiss_autosave_or_lock",{timeout:500,data:{wp_customize:"on",customize_theme:Y.settings.theme.stylesheet,customize_changeset_uuid:Y.settings.changeset.uuid,nonce:Y.settings.nonce.dismiss_autosave_or_lock,dismiss_autosave:t,dismiss_lock:n}}).always(function(){e.resolve()}),e.promise()}Y.settings=window._wpCustomizeSettings,Y.l10n=window._wpCustomizeControlsL10n,Y.settings&&J.support.postMessage&&(J.support.cors||!Y.settings.isCrossDomain)&&(null===Y.PreviewFrame.prototype.sensitivity&&(Y.PreviewFrame.prototype.sensitivity=Y.settings.timeouts.previewFrameSensitivity),null===Y.Previewer.prototype.refreshBuffer&&(Y.Previewer.prototype.refreshBuffer=Y.settings.timeouts.windowRefresh),o=J(document.body),t=o.children(".wp-full-overlay"),n=J("#customize-info .panel-title.site-title"),i=J(".customize-controls-close"),d=J("#save"),u=J("#customize-save-button-wrapper"),p=J("#publish-settings"),a=J("#customize-footer-actions"),Y.bind("ready",function(){Y.section.add(new Y.OuterSection("publish_settings",{title:Y.l10n.publishSettings,priority:0,active:Y.settings.theme.active}))}),Y.section("publish_settings",function(t){var e,n,i,a,o,s,r;function c(){r=r||Y.utils.highlightButton(u,{delay:1e3,focusTarget:d})}function l(){r&&(r(),r=null)}e=new Y.Control("trash_changeset",{type:"button",section:t.id,priority:30,input_attrs:{class:"button-link button-link-delete",value:Y.l10n.discardChanges}}),Y.control.add(e),e.deferred.embedded.done(function(){e.container.find(".button-link").on("click",function(){confirm(Y.l10n.trashConfirm)&&wp.customize.previewer.trash()})}),Y.control.add(new Y.PreviewLinkControl("changeset_preview_link",{section:t.id,priority:100})),t.active.validate=n=function(){return!!Y.state("activated").get()&&!(Y.state("trashing").get()||"trash"===Y.state("changesetStatus").get()||""===Y.state("changesetStatus").get()&&Y.state("saved").get())},s=function(){t.active.set(n())},Y.state("activated").bind(s),Y.state("trashing").bind(s),Y.state("saved").bind(s),Y.state("changesetStatus").bind(s),s(),(s=function(){p.toggle(t.active.get()),d.toggleClass("has-next-sibling",t.active.get())})(),t.active.bind(s),Y.state("selectedChangesetStatus").bind(l),t.contentContainer.find(".customize-action").text(Y.l10n.updating),t.contentContainer.find(".customize-section-back").removeAttr("tabindex"),p.prop("disabled",!1),p.on("click",function(e){e.preventDefault(),t.expanded.set(!t.expanded.get())}),t.expanded.bind(function(e){p.attr("aria-expanded",String(e)),p.toggleClass("active",e),e?l():(""!==(e=Y.state("changesetStatus").get())&&"auto-draft"!==e||(e="publish"),(Y.state("selectedChangesetStatus").get()!==e||"future"===Y.state("selectedChangesetStatus").get()&&Y.state("selectedChangesetDate").get()!==Y.state("changesetDate").get())&&c())}),s=new Y.Control("changeset_status",{priority:10,type:"radio",section:"publish_settings",setting:Y.state("selectedChangesetStatus"),templateId:"customize-selected-changeset-status-control",label:Y.l10n.action,choices:Y.settings.changeset.statusChoices}),Y.control.add(s),(i=new Y.DateTimeControl("changeset_scheduled_date",{priority:20,section:"publish_settings",setting:Y.state("selectedChangesetDate"),minYear:(new Date).getFullYear(),allowPastDate:!1,includeTime:!0,twelveHourFormat:/a/i.test(Y.settings.timeFormat),description:Y.l10n.scheduleDescription})).notifications.alt=!0,Y.control.add(i),a=function(){Y.state("selectedChangesetStatus").set("publish"),Y.previewer.save()},s=function(){var e="future"===Y.state("changesetStatus").get()&&"future"===Y.state("selectedChangesetStatus").get()&&Y.state("changesetDate").get()&&Y.state("selectedChangesetDate").get()===Y.state("changesetDate").get()&&0<=Y.utils.getRemainingTime(Y.state("changesetDate").get());e&&!o?o=setInterval(function(){var e=Y.utils.getRemainingTime(Y.state("changesetDate").get());Y.state("remainingTimeToPublish").set(e),e<=0&&(clearInterval(o),o=0,a())},1e3):!e&&o&&(clearInterval(o),o=0)},Y.state("changesetDate").bind(s),Y.state("selectedChangesetDate").bind(s),Y.state("changesetStatus").bind(s),Y.state("selectedChangesetStatus").bind(s),s(),i.active.validate=function(){return"future"===Y.state("selectedChangesetStatus").get()},(s=function(e){i.active.set("future"===e)})(Y.state("selectedChangesetStatus").get()),Y.state("selectedChangesetStatus").bind(s),Y.state("saving").bind(function(e){e&&"future"===Y.state("selectedChangesetStatus").get()&&i.toggleFutureDateNotification(!i.isFutureDate())})}),J("#customize-controls").on("keydown",function(e){var t=13===e.which,n=J(e.target);t&&(n.is("input:not([type=button])")||n.is("select"))&&e.preventDefault()}),J(".customize-info").find("> .accordion-section-title .customize-help-toggle").on("click",function(){var e=J(this).closest(".accordion-section"),t=e.find(".customize-panel-description:first");e.hasClass("cannot-expand")||(e.hasClass("open")?(e.toggleClass("open"),t.slideUp(Y.Panel.prototype.defaultExpandedArguments.duration,function(){t.trigger("toggled")}),J(this).attr("aria-expanded",!1)):(t.slideDown(Y.Panel.prototype.defaultExpandedArguments.duration,function(){t.trigger("toggled")}),e.toggleClass("open"),J(this).attr("aria-expanded",!0)))}),Y.previewer=new Y.Previewer({container:"#customize-preview",form:"#customize-controls",previewUrl:Y.settings.url.preview,allowedUrls:Y.settings.url.allowed},{nonce:Y.settings.nonce,query:function(e){var t={wp_customize:"on",customize_theme:Y.settings.theme.stylesheet,nonce:this.nonce.preview,customize_changeset_uuid:Y.settings.changeset.uuid};return!Y.settings.changeset.autosaved&&Y.state("saved").get()||(t.customize_autosaved="on"),t.customized=JSON.stringify(Y.dirtyValues({unsaved:e&&e.excludeCustomizedSaved})),t},save:function(i){var e,t,a=this,o=J.Deferred(),s=Y.state("selectedChangesetStatus").get(),r=Y.state("selectedChangesetDate").get(),n=Y.state("processing"),c={},l=[],d=[],u=[];function p(e){c[e.id]=!0}return i&&i.status&&(s=i.status),Y.state("saving").get()&&(o.reject("already_saving"),o.promise()),Y.state("saving").set(!0),t=function(){var n={},t=Y._latestRevision,e="client_side_error";if(Y.bind("change",p),Y.notifications.remove(e),Y.each(function(t){t.notifications.each(function(e){"error"!==e.type||e.fromServer||(l.push(t.id),n[t.id]||(n[t.id]={}),n[t.id][e.code]=e)})}),Y.control.each(function(t){t.setting&&(t.setting.id||!t.active.get())||t.notifications.each(function(e){"error"===e.type&&u.push([t])})}),d=_.union(u,_.values(Y.findControlsForSettings(l))),!_.isEmpty(d))return d[0][0].focus(),Y.unbind("change",p),l.length&&Y.notifications.add(new Y.Notification(e,{message:(1===l.length?Y.l10n.saveBlockedError.singular:Y.l10n.saveBlockedError.plural).replace(/%s/g,String(l.length)),type:"error",dismissible:!0,saveFailure:!0})),o.rejectWith(a,[{setting_invalidities:n}]),Y.state("saving").set(!1),o.promise();e=J.extend(a.query({excludeCustomizedSaved:!1}),{nonce:a.nonce.save,customize_changeset_status:s}),i&&i.date?e.customize_changeset_date=i.date:"future"===s&&r&&(e.customize_changeset_date=r),i&&i.title&&(e.customize_changeset_title=i.title),Y.trigger("save-request-params",e),e=wp.ajax.post("customize_save",e),Y.state("processing").set(Y.state("processing").get()+1),Y.trigger("save",e),e.always(function(){Y.state("processing").set(Y.state("processing").get()-1),Y.state("saving").set(!1),Y.unbind("change",p)}),Y.notifications.each(function(e){e.saveFailure&&Y.notifications.remove(e.code)}),e.fail(function(e){var t,n={type:"error",dismissible:!0,fromServer:!0,saveFailure:!0};"0"===e?e="not_logged_in":"-1"===e&&(e="invalid_nonce"),"invalid_nonce"===e?a.cheatin():"not_logged_in"===e?(a.preview.iframe.hide(),a.login().done(function(){a.save(),a.preview.iframe.show()})):e.code?"not_future_date"===e.code&&Y.section.has("publish_settings")&&Y.section("publish_settings").active.get()&&Y.control.has("changeset_scheduled_date")?Y.control("changeset_scheduled_date").toggleFutureDateNotification(!0).focus():"changeset_locked"!==e.code&&(t=new Y.Notification(e.code,_.extend(n,{message:e.message}))):t=new Y.Notification("unknown_error",_.extend(n,{message:Y.l10n.unknownRequestFail})),t&&Y.notifications.add(t),e.setting_validities&&Y._handleSettingValidities({settingValidities:e.setting_validities,focusInvalidControl:!0}),o.rejectWith(a,[e]),Y.trigger("error",e),"changeset_already_published"===e.code&&e.next_changeset_uuid&&(Y.settings.changeset.uuid=e.next_changeset_uuid,Y.state("changesetStatus").set(""),Y.settings.changeset.branching&&h.send("changeset-uuid",Y.settings.changeset.uuid),Y.previewer.send("changeset-uuid",Y.settings.changeset.uuid))}),e.done(function(e){a.send("saved",e),Y.state("changesetStatus").set(e.changeset_status),e.changeset_date&&Y.state("changesetDate").set(e.changeset_date),"publish"===e.changeset_status&&(Y.each(function(e){e._dirty&&(_.isUndefined(Y._latestSettingRevisions[e.id])||Y._latestSettingRevisions[e.id]<=t)&&(e._dirty=!1)}),Y.state("changesetStatus").set(""),Y.settings.changeset.uuid=e.next_changeset_uuid,Y.settings.changeset.branching)&&h.send("changeset-uuid",Y.settings.changeset.uuid),Y._lastSavedRevision=Math.max(t,Y._lastSavedRevision),e.setting_validities&&Y._handleSettingValidities({settingValidities:e.setting_validities,focusInvalidControl:!0}),o.resolveWith(a,[e]),Y.trigger("saved",e),_.isEmpty(c)||Y.state("saved").set(!1)})},0===n()?t():(e=function(){0===n()&&(Y.state.unbind("change",e),t())},Y.state.bind("change",e)),o.promise()},trash:function(){var e,n,i;Y.state("trashing").set(!0),Y.state("processing").set(Y.state("processing").get()+1),e=wp.ajax.post("customize_trash",{customize_changeset_uuid:Y.settings.changeset.uuid,nonce:Y.settings.nonce.trash}),Y.notifications.add(new Y.OverlayNotification("changeset_trashing",{type:"info",message:Y.l10n.revertingChanges,loading:!0})),n=function(){var e,t=document.createElement("a");Y.state("changesetStatus").set("trash"),Y.each(function(e){e._dirty=!1}),Y.state("saved").set(!0),t.href=location.href,delete(e=Y.utils.parseQueryString(t.search.substr(1))).changeset_uuid,e.return=Y.settings.url.return,t.search=J.param(e),location.replace(t.href)},i=function(e,t){e=e||"unknown_error";Y.state("processing").set(Y.state("processing").get()-1),Y.state("trashing").set(!1),Y.notifications.remove("changeset_trashing"),Y.notifications.add(new Y.Notification(e,{message:t||Y.l10n.unknownError,dismissible:!0,type:"error"}))},e.done(function(e){n(e.message)}),e.fail(function(e){var t=e.code||"trashing_failed";e.success||"non_existent_changeset"===t||"changeset_already_trashed"===t?n(e.message):i(t,e.message)})},getFrontendPreviewUrl:function(){var e,t=document.createElement("a");return t.href=this.previewUrl.get(),e=Y.utils.parseQueryString(t.search.substr(1)),Y.state("changesetStatus").get()&&"publish"!==Y.state("changesetStatus").get()&&(e.customize_changeset_uuid=Y.settings.changeset.uuid),Y.state("activated").get()||(e.customize_theme=Y.settings.theme.stylesheet),t.search=J.param(e),t.href}}),J.ajaxPrefilter(function(e){/wp_customize=on/.test(e.data)&&(e.data+="&"+J.param({customize_preview_nonce:Y.settings.nonce.preview}))}),Y.previewer.bind("nonce",function(e){J.extend(this.nonce,e)}),Y.bind("nonce-refresh",function(e){J.extend(Y.settings.nonce,e),J.extend(Y.previewer.nonce,e),Y.previewer.send("nonce-refresh",e)}),J.each(Y.settings.settings,function(e,t){var n=Y.settingConstructor[t.type]||Y.Setting;Y.add(new n(e,t.value,{transport:t.transport,previewer:Y.previewer,dirty:!!t.dirty}))}),J.each(Y.settings.panels,function(e,t){var n=Y.panelConstructor[t.type]||Y.Panel,t=_.extend({params:t},t);Y.panel.add(new n(e,t))}),J.each(Y.settings.sections,function(e,t){var n=Y.sectionConstructor[t.type]||Y.Section,t=_.extend({params:t},t);Y.section.add(new n(e,t))}),J.each(Y.settings.controls,function(e,t){var n=Y.controlConstructor[t.type]||Y.Control,t=_.extend({params:t},t);Y.control.add(new n(e,t))}),_.each(["panel","section","control"],function(e){var t=Y.settings.autofocus[e];t&&Y[e](t,function(e){e.deferred.embedded.done(function(){Y.previewer.deferred.active.done(function(){e.focus()})})})}),Y.bind("ready",Y.reflowPaneContents),J([Y.panel,Y.section,Y.control]).each(function(e,t){var n=_.debounce(Y.reflowPaneContents,Y.settings.timeouts.reflowPaneContents);t.bind("add",n),t.bind("change",n),t.bind("remove",n)}),Y.bind("ready",function(){var e,t,n;Y.notifications.container=J("#customize-notifications-area"),Y.notifications.bind("change",_.debounce(function(){Y.notifications.render()})),e=J(".wp-full-overlay-sidebar-content"),Y.notifications.bind("rendered",function(){e.css("top",""),0!==Y.notifications.count()&&(t=Y.notifications.container.outerHeight()+1,n=parseInt(e.css("top"),10),e.css("top",n+t+"px")),Y.notifications.trigger("sidebarTopUpdated")}),Y.notifications.render()}),s=Y.state,c=s.instance("saved"),l=s.instance("saving"),f=s.instance("trashing"),m=s.instance("activated"),e=s.instance("processing"),I=s.instance("paneVisible"),H=s.instance("expandedPanel"),L=s.instance("expandedSection"),g=s.instance("changesetStatus"),v=s.instance("selectedChangesetStatus"),w=s.instance("changesetDate"),b=s.instance("selectedChangesetDate"),M=s.instance("previewerAlive"),O=s.instance("editShortcutVisibility"),C=s.instance("changesetLocked"),s.bind("change",function(){var e;m()?""===g.get()&&c()?(Y.settings.changeset.currentUserCanPublish?d.val(Y.l10n.published):d.val(Y.l10n.saved),i.find(".screen-reader-text").text(Y.l10n.close)):("draft"===v()?c()&&v()===g()?d.val(Y.l10n.draftSaved):d.val(Y.l10n.saveDraft):"future"===v()?!c()||v()!==g()||w.get()!==b.get()?d.val(Y.l10n.schedule):d.val(Y.l10n.scheduled):Y.settings.changeset.currentUserCanPublish&&d.val(Y.l10n.publish),i.find(".screen-reader-text").text(Y.l10n.cancel)):(d.val(Y.l10n.activate),i.find(".screen-reader-text").text(Y.l10n.cancel)),e=!l()&&!f()&&!C()&&(!m()||!c()||g()!==v()&&""!==g()||"future"===v()&&w.get()!==b.get()),d.prop("disabled",!e)}),v.validate=function(e){return""===e||"auto-draft"===e?null:e},S=Y.settings.changeset.currentUserCanPublish?"publish":"draft",g(Y.settings.changeset.status),C(Boolean(Y.settings.changeset.lockUser)),w(Y.settings.changeset.publishDate),b(Y.settings.changeset.publishDate),v(""===Y.settings.changeset.status||"auto-draft"===Y.settings.changeset.status?S:Y.settings.changeset.status),v.link(g),c(!0),""===g()&&Y.each(function(e){e._dirty&&c(!1)}),l(!1),m(Y.settings.theme.active),e(0),I(!0),H(!1),L(!1),M(!0),O("visible"),Y.bind("change",function(){s("saved").get()&&s("saved").set(!1)}),Y.settings.changeset.branching&&c.bind(function(e){e||r(!0)}),l.bind(function(e){o.toggleClass("saving",e)}),f.bind(function(e){o.toggleClass("trashing",e)}),Y.bind("saved",function(e){s("saved").set(!0),"publish"===e.changeset_status&&s("activated").set(!0)}),m.bind(function(e){e&&Y.trigger("activated")}),r=function(e){var t,n;if(history.replaceState){if((t=document.createElement("a")).href=location.href,n=Y.utils.parseQueryString(t.search.substr(1)),e){if(n.changeset_uuid===Y.settings.changeset.uuid)return;n.changeset_uuid=Y.settings.changeset.uuid}else{if(!n.changeset_uuid)return;delete n.changeset_uuid}t.search=J.param(n),history.replaceState({},document.title,t.href)}},Y.settings.changeset.branching&&g.bind(function(e){r(""!==e&&"publish"!==e&&"trash"!==e)}),j=Y.OverlayNotification.extend({templateId:"customize-changeset-locked-notification",lockUser:null,initialize:function(e,t){e=e||"changeset_locked",t=_.extend({message:"",type:"warning",containerClasses:"",lockUser:{}},t);t.containerClasses+=" notification-changeset-locked",Y.OverlayNotification.prototype.initialize.call(this,e,t)},render:function(){var t,n,i=this,e=_.extend({allowOverride:!1,returnUrl:Y.settings.url.return,previewUrl:Y.previewer.previewUrl.get(),frontendPreviewUrl:Y.previewer.getFrontendPreviewUrl()},this),a=Y.OverlayNotification.prototype.render.call(e);return Y.requestChangesetUpdate({},{autosave:!0}).fail(function(e){e.autosaved||a.find(".notice-error").prop("hidden",!1).text(e.message||Y.l10n.unknownRequestFail)}),(t=a.find(".customize-notice-take-over-button")).on("click",function(e){e.preventDefault(),n||(t.addClass("disabled"),(n=wp.ajax.post("customize_override_changeset_lock",{wp_customize:"on",customize_theme:Y.settings.theme.stylesheet,customize_changeset_uuid:Y.settings.changeset.uuid,nonce:Y.settings.nonce.override_lock})).done(function(){Y.notifications.remove(i.code),Y.state("changesetLocked").set(!1)}),n.fail(function(e){e=e.message||Y.l10n.unknownRequestFail;a.find(".notice-error").prop("hidden",!1).text(e),n.always(function(){t.removeClass("disabled")})}),n.always(function(){n=null}))}),a}}),Y.settings.changeset.lockUser&&F({allowOverride:!0}),J(document).on("heartbeat-send.update_lock_notice",function(e,t){t.check_changeset_lock=!0,t.changeset_uuid=Y.settings.changeset.uuid}),J(document).on("heartbeat-tick.update_lock_notice",function(e,t){var n,i="changeset_locked";t.customize_changeset_lock_user&&((n=Y.notifications(i))&&n.lockUser.id!==Y.settings.changeset.lockUser.id&&Y.notifications.remove(i),F({lockUser:t.customize_changeset_lock_user}))}),Y.bind("error",function(e){"changeset_locked"===e.code&&e.lock_user&&F({lockUser:e.lock_user})}),T=!(S=[]),Y.settings.changeset.autosaved&&(Y.state("saved").set(!1),S.push("customize_autosaved")),Y.settings.changeset.branching||Y.settings.changeset.status&&"auto-draft"!==Y.settings.changeset.status||S.push("changeset_uuid"),0<S.length&&(S=S,e=document.createElement("a"),x=0,e.href=location.href,y=Y.utils.parseQueryString(e.search.substr(1)),_.each(S,function(e){void 0!==y[e]&&(x+=1,delete y[e])}),0!==x)&&(e.search=J.param(y),history.replaceState({},document.title,e.href)),(Y.settings.changeset.latestAutoDraftUuid||Y.settings.changeset.hasAutosaveRevision)&&(z="autosave_available",Y.notifications.add(new Y.Notification(z,{message:Y.l10n.autosaveNotice,type:"warning",dismissible:!0,render:function(){var e=Y.Notification.prototype.render.call(this),t=e.find("a");return t.prop("href",q()),t.on("click",function(e){e.preventDefault(),location.replace(q())}),e.find(".notice-dismiss").on("click",Q),e}})),Y.bind("change",k=function(){Q(),Y.notifications.remove(z),Y.unbind("change",k),Y.state("changesetStatus").unbind(k)}),Y.state("changesetStatus").bind(k)),parseInt(J("#customize-info").data("block-theme"),10)&&(S=Y.l10n.blockThemeNotification,Y.notifications.add(new Y.Notification("site_editor_block_theme_notice",{message:S,type:"info",dismissible:!1,render:function(){var e=Y.Notification.prototype.render.call(this),t=e.find("button.switch-to-editor");return t.on("click",function(e){e.preventDefault(),location.assign(t.data("action"))}),e}}))),Y.previewer.previewUrl()?Y.previewer.refresh():Y.previewer.previewUrl(Y.settings.url.home),d.on("click",function(e){Y.previewer.save(),e.preventDefault()}).on("keydown",function(e){9!==e.which&&(13===e.which&&Y.previewer.save(),e.preventDefault())}),i.on("keydown",function(e){9!==e.which&&(13===e.which&&this.click(),e.preventDefault())}),J(".collapse-sidebar").on("click",function(){Y.state("paneVisible").set(!Y.state("paneVisible").get())}),Y.state("paneVisible").bind(function(e){t.toggleClass("preview-only",!e),t.toggleClass("expanded",e),t.toggleClass("collapsed",!e),e?J(".collapse-sidebar").attr({"aria-expanded":"true","aria-label":Y.l10n.collapseSidebar}):J(".collapse-sidebar").attr({"aria-expanded":"false","aria-label":Y.l10n.expandSidebar})}),o.on("keydown",function(e){var t,n=[],i=[],a=[];27===e.which&&(J(e.target).is("body")||J.contains(J("#customize-controls")[0],e.target))&&null===e.target.closest(".block-editor-writing-flow")&&null===e.target.closest(".block-editor-block-list__block-popover")&&(Y.control.each(function(e){e.expanded&&e.expanded()&&_.isFunction(e.collapse)&&n.push(e)}),Y.section.each(function(e){e.expanded()&&i.push(e)}),Y.panel.each(function(e){e.expanded()&&a.push(e)}),0<n.length&&0===i.length&&(n.length=0),t=n[0]||i[0]||a[0])&&("themes"===t.params.type?o.hasClass("modal-open")?t.closeDetails():Y.panel.has("themes")&&Y.panel("themes").collapse():(t.collapse(),e.preventDefault()))}),J(".customize-controls-preview-toggle").on("click",function(){Y.state("paneVisible").set(!Y.state("paneVisible").get())}),P=J(".wp-full-overlay-sidebar-content"),I=function(e){var t=Y.state("expandedSection").get(),n=Y.state("expandedPanel").get();if(D&&D.element&&(R(D.element),D.element.find(".description").off("toggled",E)),!e)if(!t&&n&&n.contentContainer)e=n;else{if(n||!t||!t.contentContainer)return void(D=!1);e=t}(n=e.contentContainer.find(".customize-section-title, .panel-meta").first()).length?((D={instance:e,element:n,parent:n.closest(".customize-pane-child"),height:n.outerHeight()}).element.find(".description").on("toggled",E),t&&B(D.element,D.parent)):D=!1},Y.state("expandedSection").bind(I),Y.state("expandedPanel").bind(I),P.on("scroll",_.throttle(function(){var e,t;D&&(e=P.scrollTop(),t=N?e===N?0:N<e?1:-1:1,N=e,0!==t)&&W(D,e,t)},8)),Y.notifications.bind("sidebarTopUpdated",function(){D&&D.element.hasClass("is-sticky")&&D.element.css("top",P.css("top"))}),R=function(e){e.hasClass("is-sticky")&&e.removeClass("is-sticky").addClass("maybe-sticky is-in-view").css("top",P.scrollTop()+"px")},B=function(e,t){e.hasClass("is-in-view")&&(e.removeClass("maybe-sticky is-in-view").css({width:"",top:""}),t.css("padding-top",""))},E=function(){D.height=D.element.outerHeight()},W=function(e,t,n){var i=e.element,a=e.parent,e=e.height,o=parseInt(i.css("top"),10),s=i.hasClass("maybe-sticky"),r=i.hasClass("is-sticky"),c=i.hasClass("is-in-view");if(-1===n){if(!s&&e<=t)s=!0,i.addClass("maybe-sticky");else if(0===t)return i.removeClass("maybe-sticky is-in-view is-sticky").css({top:"",width:""}),void a.css("padding-top","");c&&!r?t<=o&&i.addClass("is-sticky").css({top:P.css("top"),width:a.outerWidth()+"px"}):s&&!c&&(i.addClass("is-in-view").css("top",t-e+"px"),a.css("padding-top",e+"px"))}else r&&(o=t,i.removeClass("is-sticky").css({top:o+"px",width:""})),c&&o+e<t&&(i.removeClass("is-in-view"),a.css("padding-top",""))},Y.previewedDevice=Y.state("previewedDevice"),Y.bind("ready",function(){_.find(Y.settings.previewableDevices,function(e,t){if(!0===e.default)return Y.previewedDevice.set(t),!0})}),a.find(".devices button").on("click",function(e){Y.previewedDevice.set(J(e.currentTarget).data("device"))}),Y.previewedDevice.bind(function(e){var t=J(".wp-full-overlay"),n="";a.find(".devices button").removeClass("active").attr("aria-pressed",!1),a.find(".devices .preview-"+e).addClass("active").attr("aria-pressed",!0),J.each(Y.settings.previewableDevices,function(e){n+=" preview-"+e}),t.removeClass(n).addClass("preview-"+e)}),n.length&&Y("blogname",function(t){function e(){var e=t()||"";n.text(e.toString().trim()||Y.l10n.untitledBlogName)}t.bind(e),e()}),h=new Y.Messenger({url:Y.settings.url.parent,channel:"loader"}),U=!1,h.bind("back",function(){U=!0}),Y.bind("change",V),Y.state("selectedChangesetStatus").bind(V),Y.state("selectedChangesetDate").bind(V),h.bind("confirm-close",function(){$().done(function(){h.send("confirmed-close",!0)}).fail(function(){h.send("confirmed-close",!1)})}),i.on("click.customize-controls-close",function(e){e.preventDefault(),U?h.send("close"):$().done(function(){J(window).off("beforeunload.customize-confirm"),window.location.href=i.prop("href")})}),J.each(["saved","change"],function(e,t){Y.bind(t,function(){h.send(t)})}),Y.bind("title",function(e){h.send("title",e)}),Y.settings.changeset.branching&&h.send("changeset-uuid",Y.settings.changeset.uuid),h.send("ready"),J.each({background_image:{controls:["background_preset","background_position","background_size","background_repeat","background_attachment"],callback:function(e){return!!e}},show_on_front:{controls:["page_on_front","page_for_posts"],callback:function(e){return"page"===e}},header_textcolor:{controls:["header_textcolor"],callback:function(e){return"blank"!==e}}},function(e,i){Y(e,function(n){J.each(i.controls,function(e,t){Y.control(t,function(t){function e(e){t.container.toggle(i.callback(e))}e(n.get()),n.bind(e)})})})}),Y.control("background_preset",function(e){var i={default:[!1,!1,!1,!1],fill:[!0,!1,!1,!1],fit:[!0,!1,!0,!1],repeat:[!0,!1,!1,!0],custom:[!0,!0,!0,!0]},a={default:[_wpCustomizeBackground.defaults["default-position-x"],_wpCustomizeBackground.defaults["default-position-y"],_wpCustomizeBackground.defaults["default-size"],_wpCustomizeBackground.defaults["default-repeat"],_wpCustomizeBackground.defaults["default-attachment"]],fill:["left","top","cover","no-repeat","fixed"],fit:["left","top","contain","no-repeat","fixed"],repeat:["left","top","auto","repeat","scroll"]},t=function(n){_.each(["background_position","background_size","background_repeat","background_attachment"],function(e,t){e=Y.control(e);e&&e.container.toggle(i[n][t])})},n=function(n){_.each(["background_position_x","background_position_y","background_size","background_repeat","background_attachment"],function(e,t){e=Y(e);e&&e.set(a[n][t])})},o=e.setting.get();t(o),e.setting.bind("change",function(e){t(e),"custom"!==e&&n(e)})}),Y.control("background_repeat",function(t){t.elements[0].unsync(Y("background_repeat")),t.element=new Y.Element(t.container.find("input")),t.element.set("no-repeat"!==t.setting()),t.element.bind(function(e){t.setting.set(e?"repeat":"no-repeat")}),t.setting.bind(function(e){t.element.set("no-repeat"!==e)})}),Y.control("background_attachment",function(t){t.elements[0].unsync(Y("background_attachment")),t.element=new Y.Element(t.container.find("input")),t.element.set("fixed"!==t.setting()),t.element.bind(function(e){t.setting.set(e?"scroll":"fixed")}),t.setting.bind(function(e){t.element.set("fixed"!==e)})}),Y.control("display_header_text",function(t){var n="";t.elements[0].unsync(Y("header_textcolor")),t.element=new Y.Element(t.container.find("input")),t.element.set("blank"!==t.setting()),t.element.bind(function(e){e||(n=Y("header_textcolor").get()),t.setting.set(e?n:"blank")}),t.setting.bind(function(e){t.element.set("blank"!==e)})}),Y("show_on_front","page_on_front","page_for_posts",function(i,a,o){function e(){var e="show_on_front_page_collision",t=parseInt(a(),10),n=parseInt(o(),10);"page"===i()&&(this===a&&0<t&&Y.previewer.previewUrl.set(Y.settings.url.home),this===o)&&0<n&&Y.previewer.previewUrl.set(Y.settings.url.home+"?page_id="+n),"page"===i()&&t&&n&&t===n?i.notifications.add(new Y.Notification(e,{type:"error",message:Y.l10n.pageOnFrontError})):i.notifications.remove(e)}i.bind(e),a.bind(e),o.bind(e),e.call(i,i()),Y.control("show_on_front",function(e){e.deferred.embedded.done(function(){e.container.append(e.getNotificationsContainerElement())})})}),A=J.Deferred(),Y.section("custom_css",function(t){t.deferred.embedded.done(function(){t.expanded()?A.resolve(t):t.expanded.bind(function(e){e&&A.resolve(t)})})}),A.done(function(e){var t=Y.control("custom_css");t.container.find(".customize-control-title:first").addClass("screen-reader-text"),e.container.find(".section-description-buttons .section-description-close").on("click",function(){e.container.find(".section-meta .customize-section-description:first").removeClass("open").slideUp(),e.container.find(".customize-help-toggle").attr("aria-expanded","false").focus()}),t&&!t.setting.get()&&(e.container.find(".section-meta .customize-section-description:first").addClass("open").show().trigger("toggled"),e.container.find(".customize-help-toggle").attr("aria-expanded","true"))}),Y.control("header_video",function(n){n.deferred.embedded.done(function(){function e(){var e=Y.section(n.section()),t="video_header_not_available";e&&(n.active.get()?e.notifications.remove(t):e.notifications.add(new Y.Notification(t,{type:"info",message:Y.l10n.videoHeaderNotice})))}e(),n.active.bind(e)})}),Y.previewer.bind("selective-refresh-setting-validities",function(e){Y._handleSettingValidities({settingValidities:e,focusInvalidControl:!1})}),Y.previewer.bind("focus-control-for-setting",function(n){var i=[];Y.control.each(function(e){var t=_.pluck(e.settings,"id");-1!==_.indexOf(t,n)&&i.push(e)}),i.length&&(i.sort(function(e,t){return e.priority()-t.priority()}),i[0].focus())}),Y.previewer.bind("refresh",function(){Y.previewer.refresh()}),Y.state("paneVisible").bind(function(e){var t=window.matchMedia?window.matchMedia("screen and ( max-width: 640px )").matches:J(window).width()<=640;Y.state("editShortcutVisibility").set(e||t?"visible":"hidden")}),window.matchMedia&&window.matchMedia("screen and ( max-width: 640px )").addListener(function(){var e=Y.state("paneVisible");e.callbacks.fireWith(e,[e.get(),e.get()])}),Y.previewer.bind("edit-shortcut-visibility",function(e){Y.state("editShortcutVisibility").set(e)}),Y.state("editShortcutVisibility").bind(function(e){Y.previewer.send("edit-shortcut-visibility",e)}),Y.bind("change",function e(){var t,n,i,a=!1;function o(e){e||Y.settings.changeset.autosaved||(Y.settings.changeset.autosaved=!0,Y.previewer.send("autosaving"))}Y.unbind("change",e),Y.state("saved").bind(o),o(Y.state("saved").get()),n=function(){a||(a=!0,Y.requestChangesetUpdate({},{autosave:!0}).always(function(){a=!1})),i()},(i=function(){clearTimeout(t),t=setTimeout(function(){n()},Y.settings.timeouts.changesetAutoSave)})(),J(document).on("visibilitychange.wp-customize-changeset-update",function(){document.hidden&&n()}),J(window).on("beforeunload.wp-customize-changeset-update",function(){n()})}),J(document).one("tinymce-editor-setup",function(){window.tinymce.ui.FloatPanel&&(!window.tinymce.ui.FloatPanel.zIndex||window.tinymce.ui.FloatPanel.zIndex<500001)&&(window.tinymce.ui.FloatPanel.zIndex=500001)}),o.addClass("ready"),Y.trigger("ready"))})}((wp,jQuery));
\ No newline at end of file diff --git a/wp-admin/js/customize-nav-menus.js b/wp-admin/js/customize-nav-menus.js new file mode 100644 index 0000000..8930f15 --- /dev/null +++ b/wp-admin/js/customize-nav-menus.js @@ -0,0 +1,3429 @@ +/** + * @output wp-admin/js/customize-nav-menus.js + */ + +/* global _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( $( '<li class="nothing-found"></li>' ).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.<Object>} 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:first' ); + $title.find( '.menu-in-location' ).remove(); + _.each( themeLocationSlugs, function( themeLocationSlug ) { + var $label, locationName; + $label = $( '<span class="menu-in-location"></span>' ); + 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' ); + + _.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 <select>). + * Note that 'nav_menu_location' must match the WP_Customize_Nav_Menu_Location_Control::$type. + * + * @class wp.customize.Menus.MenuLocationControl + * @augments wp.customize.Control + */ + api.Menus.MenuLocationControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationControl.prototype */{ + initialize: function( id, options ) { + var control = this, + matches = id.match( /^nav_menu_locations\[(.+?)]/ ); + control.themeLocation = matches[1]; + api.Control.prototype.initialize.call( control, id, options ); + }, + + ready: function() { + var control = this, navMenuIdRegex = /^nav_menu\[(-?\d+)]/; + + // @todo It would be better if this was added directly on the setting itself, as opposed to the control. + control.setting.validate = function( value ) { + if ( '' === value ) { + return 0; + } else { + return parseInt( value, 10 ); + } + }; + + // Create and Edit menu buttons. + control.container.find( '.create-menu' ).on( 'click', function() { + var addMenuSection = api.section( 'add_menu' ); + addMenuSection.selectDefaultLocation( this.dataset.locationId ); + addMenuSection.focus(); + } ); + control.container.find( '.edit-menu' ).on( 'click', function() { + var menuId = control.setting(); + api.section( 'nav_menu[' + menuId + ']' ).focus(); + }); + control.setting.bind( 'change', function() { + var menuIsSelected = 0 !== control.setting(); + control.container.find( '.create-menu' ).toggleClass( 'hidden', menuIsSelected ); + control.container.find( '.edit-menu' ).toggleClass( 'hidden', ! menuIsSelected ); + }); + + // Add/remove menus from the available options when they are added and removed. + api.bind( 'add', function( setting ) { + var option, menuId, matches = setting.id.match( navMenuIdRegex ); + if ( ! matches || false === setting() ) { + return; + } + menuId = matches[1]; + option = new Option( displayNavMenuName( setting().name ), menuId ); + control.container.find( 'select' ).append( option ); + }); + api.bind( 'remove', function( setting ) { + var menuId, matches = setting.id.match( navMenuIdRegex ); + if ( ! matches ) { + return; + } + menuId = parseInt( matches[1], 10 ); + if ( control.setting() === menuId ) { + control.setting.set( '' ); + } + control.container.find( 'option[value=' + menuId + ']' ).remove(); + }); + api.bind( 'change', function( setting ) { + var menuId, matches = setting.id.match( navMenuIdRegex ); + if ( ! matches ) { + return; + } + menuId = parseInt( matches[1], 10 ); + if ( false === setting() ) { + if ( control.setting() === menuId ) { + control.setting.set( '' ); + } + control.container.find( 'option[value=' + menuId + ']' ).remove(); + } else { + control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) ); + } + }); + } + }); + + api.Menus.MenuItemControl = api.Control.extend(/** @lends wp.customize.Menus.MenuItemControl.prototype */{ + + /** + * wp.customize.Menus.MenuItemControl + * + * Customizer control for menu items. + * Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type. + * + * @constructs wp.customize.Menus.MenuItemControl + * @augments wp.customize.Control + * + * @inheritDoc + */ + initialize: function( id, options ) { + var control = this; + 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 ); + }); + api.Control.prototype.initialize.call( control, id, options ); + control.active.validate = function() { + var value, section = api.section( control.section() ); + if ( section ) { + value = section.active(); + } else { + value = false; + } + return value; + }; + }, + + /** + * Override the embed() method to do nothing, + * so that the control isn't embedded on load, + * unless the containing section is already expanded. + * + * @since 4.3.0 + */ + embed: function() { + var control = this, + sectionId = control.section(), + section; + if ( ! sectionId ) { + return; + } + section = api.section( sectionId ); + if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) { + control.actuallyEmbed(); + } + }, + + /** + * This function is called in Section.onChangeExpanded() so the control + * will only get embedded when the Section is first expanded. + * + * @since 4.3.0 + */ + actuallyEmbed: function() { + var control = this; + if ( 'resolved' === control.deferred.embedded.state() ) { + return; + } + control.renderContent(); + control.deferred.embedded.resolve(); // This triggers control.ready(). + }, + + /** + * Set up the control. + */ + ready: function() { + if ( 'undefined' === typeof this.params.menu_item_id ) { + throw new Error( 'params.menu_item_id was not defined' ); + } + + this._setupControlToggle(); + this._setupReorderUI(); + this._setupUpdateUI(); + this._setupRemoveUI(); + this._setupLinksUI(); + this._setupTitleUI(); + }, + + /** + * Show/hide the settings when clicking on the menu item handle. + */ + _setupControlToggle: function() { + var control = this; + + this.container.find( '.menu-item-handle' ).on( 'click', function( e ) { + e.preventDefault(); + e.stopPropagation(); + var menuControl = control.getMenuControl(), + 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 ) { + api.Menus.availableMenuItemsPanel.close(); + } + + if ( menuControl.isReordering || menuControl.isSorting ) { + return; + } + control.toggleForm(); + } ); + }, + + /** + * Set up the menu-item-reorder-nav + */ + _setupReorderUI: function() { + var control = this, template, $reorderNav; + + template = wp.template( 'menu-item-reorder-nav' ); + + // Add the menu item reordering elements to the menu item control. + control.container.find( '.item-controls' ).after( template ); + + // Handle clicks for up/down/left-right on the reorder nav. + $reorderNav = control.container.find( '.menu-item-reorder-nav' ); + $reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() { + var moveBtn = $( this ); + moveBtn.focus(); + + var isMoveUp = moveBtn.is( '.menus-move-up' ), + isMoveDown = moveBtn.is( '.menus-move-down' ), + isMoveLeft = moveBtn.is( '.menus-move-left' ), + isMoveRight = moveBtn.is( '.menus-move-right' ); + + if ( isMoveUp ) { + control.moveUp(); + } else if ( isMoveDown ) { + control.moveDown(); + } else if ( isMoveLeft ) { + control.moveLeft(); + } else if ( isMoveRight ) { + control.moveRight(); + } + + moveBtn.focus(); // Re-focus after the container was moved. + } ); + }, + + /** + * Set up event handlers for menu item updating. + */ + _setupUpdateUI: function() { + var control = this, + settingValue = control.setting(), + updateNotifications; + + control.elements = {}; + control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) ); + control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) ); + control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) ); + control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) ); + control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) ); + control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) ); + control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) ); + // @todo Allow other elements, added by plugins, to be automatically picked up here; + // allow additional values to be added to setting array. + + _.each( control.elements, function( element, property ) { + element.bind(function( value ) { + if ( element.element.is( 'input[type=checkbox]' ) ) { + value = ( value ) ? element.element.val() : ''; + } + + var settingValue = control.setting(); + if ( settingValue && settingValue[ property ] !== value ) { + settingValue = _.clone( settingValue ); + settingValue[ property ] = value; + control.setting.set( settingValue ); + } + }); + if ( settingValue ) { + if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) { + element.set( settingValue[ property ].join( ' ' ) ); + } else { + element.set( settingValue[ property ] ); + } + } + }); + + control.setting.bind(function( to, from ) { + var itemId = control.params.menu_item_id, + followingSiblingItemControls = [], + childrenItemControls = [], + menuControl; + + if ( false === to ) { + menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' ); + control.container.remove(); + + _.each( menuControl.getMenuItemControls(), function( otherControl ) { + if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) { + followingSiblingItemControls.push( otherControl ); + } else if ( otherControl.setting().menu_item_parent === itemId ) { + childrenItemControls.push( otherControl ); + } + }); + + // Shift all following siblings by the number of children this item has. + _.each( followingSiblingItemControls, function( followingSiblingItemControl ) { + var value = _.clone( followingSiblingItemControl.setting() ); + value.position += childrenItemControls.length; + followingSiblingItemControl.setting.set( value ); + }); + + // Now move the children up to be the new subsequent siblings. + _.each( childrenItemControls, function( childrenItemControl, i ) { + var value = _.clone( childrenItemControl.setting() ); + value.position = from.position + i; + value.menu_item_parent = from.menu_item_parent; + childrenItemControl.setting.set( value ); + }); + + menuControl.debouncedReflowMenuItems(); + } else { + // Update the elements' values to match the new setting properties. + _.each( to, function( value, key ) { + if ( control.elements[ key] ) { + control.elements[ key ].set( to[ key ] ); + } + } ); + control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent ); + + // Handle UI updates when the position or depth (parent) change. + if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) { + control.getMenuControl().debouncedReflowMenuItems(); + } + } + }); + + // Style the URL field as invalid when there is an invalid_url notification. + updateNotifications = function() { + control.elements.url.element.toggleClass( 'invalid', control.setting.notifications.has( 'invalid_url' ) ); + }; + control.setting.notifications.bind( 'add', updateNotifications ); + control.setting.notifications.bind( 'removed', updateNotifications ); + }, + + /** + * Set up event handlers for menu item deletion. + */ + _setupRemoveUI: function() { + var control = this, $removeBtn; + + // Configure delete button. + $removeBtn = control.container.find( '.item-delete' ); + + $removeBtn.on( 'click', function() { + // Find an adjacent element to add focus to when this menu item goes away. + var addingItems = true, $adjacentFocusTarget, $next, $prev, + instanceCounter = 0, // Instance count of the menu item deleted. + deleteItemOriginalItemId = control.params.original_item_id, + addedItems = control.getMenuControl().$sectionContent.find( '.menu-item' ), + availableMenuItem; + + if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) { + addingItems = false; + } + + $next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first(); + $prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first(); + + if ( $next.length ) { + $adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first(); + } else if ( $prev.length ) { + $adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first(); + } else { + $adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first(); + } + + /* + * If the menu item deleted is the only of its instance left, + * remove the check icon of this menu item in the right panel. + */ + _.each( addedItems, function( addedItem ) { + var menuItemId, menuItemControl, matches; + + // This is because menu item that's deleted is just hidden. + if ( ! $( addedItem ).is( ':visible' ) ) { + return; + } + + matches = addedItem.getAttribute( 'id' ).match( /^customize-control-nav_menu_item-(-?\d+)$/, '' ); + if ( ! matches ) { + return; + } + + menuItemId = parseInt( matches[1], 10 ); + menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' ); + + // Check for duplicate menu items. + if ( menuItemControl && deleteItemOriginalItemId == menuItemControl.params.original_item_id ) { + instanceCounter++; + } + } ); + + if ( instanceCounter <= 1 ) { + // Revert the check icon to add icon. + availableMenuItem = $( '#menu-item-tpl-' + control.params.original_item_id ); + availableMenuItem.removeClass( 'selected' ); + availableMenuItem.find( '.menu-item-handle' ).removeClass( 'item-added' ); + } + + control.container.slideUp( function() { + control.setting.set( false ); + wp.a11y.speak( api.Menus.data.l10n.itemDeleted ); + $adjacentFocusTarget.focus(); // Keyboard accessibility. + } ); + + control.setting.set( false ); + } ); + }, + + _setupLinksUI: function() { + var $origBtn; + + // Configure original link. + $origBtn = this.container.find( 'a.original-link' ); + + $origBtn.on( 'click', function( e ) { + e.preventDefault(); + api.previewer.previewUrl( e.target.toString() ); + } ); + }, + + /** + * Update item handle title when changed. + */ + _setupTitleUI: function() { + var control = this, titleEl; + + // Ensure that whitespace is trimmed on blur so placeholder can be shown. + control.container.find( '.edit-menu-item-title' ).on( 'blur', function() { + $( this ).val( $( this ).val().trim() ); + } ); + + titleEl = control.container.find( '.menu-item-title' ); + control.setting.bind( function( item ) { + var trimmedTitle, titleText; + if ( ! item ) { + return; + } + item.title = item.title || ''; + trimmedTitle = item.title.trim(); + + titleText = trimmedTitle || item.original_title || api.Menus.data.l10n.untitled; + + if ( item._invalid ) { + titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText ); + } + + // Don't update to an empty title. + if ( trimmedTitle || item.original_title ) { + titleEl + .text( titleText ) + .removeClass( 'no-title' ); + } else { + titleEl + .text( titleText ) + .addClass( 'no-title' ); + } + } ); + }, + + /** + * + * @return {number} + */ + getDepth: function() { + var control = this, setting = control.setting(), depth = 0; + if ( ! setting ) { + return 0; + } + while ( setting && setting.menu_item_parent ) { + depth += 1; + control = api.control( 'nav_menu_item[' + setting.menu_item_parent + ']' ); + if ( ! control ) { + break; + } + setting = control.setting(); + } + return depth; + }, + + /** + * Amend the control's params with the data necessary for the JS template just in time. + */ + renderContent: function() { + var control = this, + settingValue = control.setting(), + containerClasses; + + control.params.title = settingValue.title || ''; + control.params.depth = control.getDepth(); + control.container.data( 'item-depth', control.params.depth ); + containerClasses = [ + 'menu-item', + 'menu-item-depth-' + String( control.params.depth ), + 'menu-item-' + settingValue.object, + 'menu-item-edit-inactive' + ]; + + if ( settingValue._invalid ) { + containerClasses.push( 'menu-item-invalid' ); + control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title ); + } else if ( 'draft' === settingValue.status ) { + containerClasses.push( 'pending' ); + control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title ); + } + + control.params.el_classes = containerClasses.join( ' ' ); + control.params.item_type_label = settingValue.type_label; + control.params.item_type = settingValue.type; + control.params.url = settingValue.url; + control.params.target = settingValue.target; + control.params.attr_title = settingValue.attr_title; + control.params.classes = _.isArray( settingValue.classes ) ? settingValue.classes.join( ' ' ) : settingValue.classes; + control.params.xfn = settingValue.xfn; + control.params.description = settingValue.description; + control.params.parent = settingValue.menu_item_parent; + control.params.original_title = settingValue.original_title || ''; + + control.container.addClass( control.params.el_classes ); + + api.Control.prototype.renderContent.call( control ); + }, + + /*********************************************************************** + * Begin public API methods + **********************************************************************/ + + /** + * @return {wp.customize.controlConstructor.nav_menu|null} + */ + getMenuControl: function() { + var control = this, settingValue = control.setting(); + if ( settingValue && settingValue.nav_menu_term_id ) { + return api.control( 'nav_menu[' + settingValue.nav_menu_term_id + ']' ); + } else { + return null; + } + }, + + /** + * Expand the accordion section containing a control + */ + expandControlSection: function() { + var $section = this.container.closest( '.accordion-section' ); + if ( ! $section.hasClass( 'open' ) ) { + $section.find( '.accordion-section-title:first' ).trigger( 'click' ); + } + }, + + /** + * @since 4.6.0 + * + * @param {Boolean} expanded + * @param {Object} [params] + * @return {Boolean} False if state already applied. + */ + _toggleExpanded: api.Section.prototype._toggleExpanded, + + /** + * @since 4.6.0 + * + * @param {Object} [params] + * @return {Boolean} False if already expanded. + */ + expand: api.Section.prototype.expand, + + /** + * Expand the menu item form control. + * + * @since 4.5.0 Added params.completeCallback. + * + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. + */ + expandForm: function( params ) { + this.expand( params ); + }, + + /** + * @since 4.6.0 + * + * @param {Object} [params] + * @return {Boolean} False if already collapsed. + */ + collapse: api.Section.prototype.collapse, + + /** + * Collapse the menu item form control. + * + * @since 4.5.0 Added params.completeCallback. + * + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. + */ + collapseForm: function( params ) { + this.collapse( params ); + }, + + /** + * Expand or collapse the menu item control. + * + * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) + * @since 4.5.0 Added params.completeCallback. + * + * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. + */ + toggleForm: function( showOrHide, params ) { + if ( typeof showOrHide === 'undefined' ) { + showOrHide = ! this.expanded(); + } + if ( showOrHide ) { + this.expand( params ); + } else { + this.collapse( params ); + } + }, + + /** + * Expand or collapse the menu item control. + * + * @since 4.6.0 + * @param {boolean} [showOrHide] - If not supplied, will be inverse of current visibility + * @param {Object} [params] - Optional params. + * @param {Function} [params.completeCallback] - Function to call when the form toggle has finished animating. + */ + onChangeExpanded: function( showOrHide, params ) { + var self = this, $menuitem, $inside, complete; + + $menuitem = this.container; + $inside = $menuitem.find( '.menu-item-settings:first' ); + if ( 'undefined' === typeof showOrHide ) { + showOrHide = ! $inside.is( ':visible' ); + } + + // Already expanded or collapsed. + if ( $inside.is( ':visible' ) === showOrHide ) { + if ( params && params.completeCallback ) { + params.completeCallback(); + } + return; + } + + if ( showOrHide ) { + // Close all other menu item controls before expanding this one. + api.control.each( function( otherControl ) { + if ( self.params.type === otherControl.params.type && self !== otherControl ) { + otherControl.collapseForm(); + } + } ); + + complete = function() { + $menuitem + .removeClass( 'menu-item-edit-inactive' ) + .addClass( 'menu-item-edit-active' ); + self.container.trigger( 'expanded' ); + + if ( params && params.completeCallback ) { + params.completeCallback(); + } + }; + + $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'true' ); + $inside.slideDown( 'fast', complete ); + + self.container.trigger( 'expand' ); + } else { + complete = function() { + $menuitem + .addClass( 'menu-item-edit-inactive' ) + .removeClass( 'menu-item-edit-active' ); + self.container.trigger( 'collapsed' ); + + if ( params && params.completeCallback ) { + params.completeCallback(); + } + }; + + self.container.trigger( 'collapse' ); + + $menuitem.find( '.item-edit' ).attr( 'aria-expanded', 'false' ); + $inside.slideUp( 'fast', complete ); + } + }, + + /** + * Expand the containing menu section, expand the form, and focus on + * the first input in the control. + * + * @since 4.5.0 Added params.completeCallback. + * + * @param {Object} [params] - Params object. + * @param {Function} [params.completeCallback] - Optional callback function when focus has completed. + */ + focus: function( params ) { + params = params || {}; + var control = this, originalCompleteCallback = params.completeCallback, focusControl; + + focusControl = function() { + control.expandControlSection(); + + params.completeCallback = function() { + var focusable; + + // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583 + focusable = control.container.find( '.menu-item-settings' ).find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ); + focusable.first().focus(); + + if ( originalCompleteCallback ) { + originalCompleteCallback(); + } + }; + + control.expandForm( params ); + }; + + if ( api.section.has( control.section() ) ) { + api.section( control.section() ).expand( { + completeCallback: focusControl + } ); + } else { + focusControl(); + } + }, + + /** + * Move menu item up one in the menu. + */ + moveUp: function() { + this._changePosition( -1 ); + wp.a11y.speak( api.Menus.data.l10n.movedUp ); + }, + + /** + * Move menu item up one in the menu. + */ + moveDown: function() { + this._changePosition( 1 ); + wp.a11y.speak( api.Menus.data.l10n.movedDown ); + }, + /** + * Move menu item and all children up one level of depth. + */ + moveLeft: function() { + this._changeDepth( -1 ); + wp.a11y.speak( api.Menus.data.l10n.movedLeft ); + }, + + /** + * Move menu item and children one level deeper, as a submenu of the previous item. + */ + moveRight: function() { + this._changeDepth( 1 ); + wp.a11y.speak( api.Menus.data.l10n.movedRight ); + }, + + /** + * Note that this will trigger a UI update, causing child items to + * move as well and cardinal order class names to be updated. + * + * @private + * + * @param {number} offset 1|-1 + */ + _changePosition: function( offset ) { + var control = this, + adjacentSetting, + settingValue = _.clone( control.setting() ), + siblingSettings = [], + realPosition; + + if ( 1 !== offset && -1 !== offset ) { + throw new Error( 'Offset changes by 1 are only supported.' ); + } + + // Skip moving deleted items. + if ( ! control.setting() ) { + return; + } + + // Locate the other items under the same parent (siblings). + _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { + if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) { + siblingSettings.push( otherControl.setting ); + } + }); + siblingSettings.sort(function( a, b ) { + return a().position - b().position; + }); + + realPosition = _.indexOf( siblingSettings, control.setting ); + if ( -1 === realPosition ) { + throw new Error( 'Expected setting to be among siblings.' ); + } + + // Skip doing anything if the item is already at the edge in the desired direction. + if ( ( realPosition === 0 && offset < 0 ) || ( realPosition === siblingSettings.length - 1 && offset > 0 ) ) { + // @todo Should we allow a menu item to be moved up to break it out of a parent? Adopt with previous or following parent? + return; + } + + // Update any adjacent menu item setting to take on this item's position. + adjacentSetting = siblingSettings[ realPosition + offset ]; + if ( adjacentSetting ) { + adjacentSetting.set( $.extend( + _.clone( adjacentSetting() ), + { + position: settingValue.position + } + ) ); + } + + settingValue.position += offset; + control.setting.set( settingValue ); + }, + + /** + * Note that this will trigger a UI update, causing child items to + * move as well and cardinal order class names to be updated. + * + * @private + * + * @param {number} offset 1|-1 + */ + _changeDepth: function( offset ) { + if ( 1 !== offset && -1 !== offset ) { + throw new Error( 'Offset changes by 1 are only supported.' ); + } + var control = this, + settingValue = _.clone( control.setting() ), + siblingControls = [], + realPosition, + siblingControl, + parentControl; + + // Locate the other items under the same parent (siblings). + _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { + if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) { + siblingControls.push( otherControl ); + } + }); + siblingControls.sort(function( a, b ) { + return a.setting().position - b.setting().position; + }); + + realPosition = _.indexOf( siblingControls, control ); + if ( -1 === realPosition ) { + throw new Error( 'Expected control to be among siblings.' ); + } + + if ( -1 === offset ) { + // Skip moving left an item that is already at the top level. + if ( ! settingValue.menu_item_parent ) { + return; + } + + parentControl = api.control( 'nav_menu_item[' + settingValue.menu_item_parent + ']' ); + + // Make this control the parent of all the following siblings. + _( siblingControls ).chain().slice( realPosition ).each(function( siblingControl, i ) { + siblingControl.setting.set( + $.extend( + {}, + siblingControl.setting(), + { + menu_item_parent: control.params.menu_item_id, + position: i + } + ) + ); + }); + + // Increase the positions of the parent item's subsequent children to make room for this one. + _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { + var otherControlSettingValue, isControlToBeShifted; + isControlToBeShifted = ( + otherControl.setting().menu_item_parent === parentControl.setting().menu_item_parent && + otherControl.setting().position > parentControl.setting().position + ); + if ( isControlToBeShifted ) { + otherControlSettingValue = _.clone( otherControl.setting() ); + otherControl.setting.set( + $.extend( + otherControlSettingValue, + { position: otherControlSettingValue.position + 1 } + ) + ); + } + }); + + // Make this control the following sibling of its parent item. + settingValue.position = parentControl.setting().position + 1; + settingValue.menu_item_parent = parentControl.setting().menu_item_parent; + control.setting.set( settingValue ); + + } else if ( 1 === offset ) { + // Skip moving right an item that doesn't have a previous sibling. + if ( realPosition === 0 ) { + return; + } + + // Make the control the last child of the previous sibling. + siblingControl = siblingControls[ realPosition - 1 ]; + settingValue.menu_item_parent = siblingControl.params.menu_item_id; + settingValue.position = 0; + _( control.getMenuControl().getMenuItemControls() ).each(function( otherControl ) { + if ( otherControl.setting().menu_item_parent === settingValue.menu_item_parent ) { + settingValue.position = Math.max( settingValue.position, otherControl.setting().position ); + } + }); + settingValue.position += 1; + control.setting.set( settingValue ); + } + } + } ); + + /** + * wp.customize.Menus.MenuNameControl + * + * Customizer control for a nav menu's name. + * + * @class wp.customize.Menus.MenuNameControl + * @augments wp.customize.Control + */ + api.Menus.MenuNameControl = api.Control.extend(/** @lends wp.customize.Menus.MenuNameControl.prototype */{ + + ready: function() { + var control = this; + + if ( control.setting ) { + var settingValue = control.setting(); + + control.nameElement = new api.Element( control.container.find( '.menu-name-field' ) ); + + control.nameElement.bind(function( value ) { + var settingValue = control.setting(); + if ( settingValue && settingValue.name !== value ) { + settingValue = _.clone( settingValue ); + settingValue.name = value; + control.setting.set( settingValue ); + } + }); + if ( settingValue ) { + control.nameElement.set( settingValue.name ); + } + + control.setting.bind(function( object ) { + if ( object ) { + control.nameElement.set( object.name ); + } + }); + } + } + }); + + /** + * wp.customize.Menus.MenuLocationsControl + * + * Customizer control for a nav menu's locations. + * + * @since 4.9.0 + * @class wp.customize.Menus.MenuLocationsControl + * @augments wp.customize.Control + */ + api.Menus.MenuLocationsControl = api.Control.extend(/** @lends wp.customize.Menus.MenuLocationsControl.prototype */{ + + /** + * Set up the control. + * + * @since 4.9.0 + */ + ready: function () { + var control = this; + + control.container.find( '.assigned-menu-location' ).each(function() { + var container = $( this ), + checkbox = container.find( 'input[type=checkbox]' ), + element = new api.Element( checkbox ), + navMenuLocationSetting = api( 'nav_menu_locations[' + checkbox.data( 'location-id' ) + ']' ), + isNewMenu = control.params.menu_id === '', + updateCheckbox = isNewMenu ? _.noop : function( checked ) { + element.set( checked ); + }, + updateSetting = isNewMenu ? _.noop : function( checked ) { + navMenuLocationSetting.set( checked ? control.params.menu_id : 0 ); + }, + updateSelectedMenuLabel = function( selectedMenuId ) { + var menuSetting = api( 'nav_menu[' + String( selectedMenuId ) + ']' ); + if ( ! selectedMenuId || ! menuSetting || ! menuSetting() ) { + container.find( '.theme-location-set' ).hide(); + } else { + container.find( '.theme-location-set' ).show().find( 'span' ).text( displayNavMenuName( menuSetting().name ) ); + } + }; + + updateCheckbox( navMenuLocationSetting.get() === control.params.menu_id ); + + checkbox.on( 'change', function() { + // Note: We can't use element.bind( function( checked ){ ... } ) here because it will trigger a change as well. + updateSetting( this.checked ); + } ); + + navMenuLocationSetting.bind( function( selectedMenuId ) { + updateCheckbox( selectedMenuId === control.params.menu_id ); + updateSelectedMenuLabel( selectedMenuId ); + } ); + updateSelectedMenuLabel( navMenuLocationSetting.get() ); + }); + }, + + /** + * Set the selected locations. + * + * This method sets the selected locations and allows us to do things like + * set the default location for a new menu. + * + * @since 4.9.0 + * + * @param {Object.<string,boolean>} selections - A map of location selections. + * @return {void} + */ + setSelections: function( selections ) { + this.container.find( '.menu-location' ).each( function( i, checkboxNode ) { + var locationId = checkboxNode.dataset.locationId; + checkboxNode.checked = locationId in selections ? selections[ locationId ] : false; + } ); + } + }); + + /** + * wp.customize.Menus.MenuAutoAddControl + * + * Customizer control for a nav menu's auto add. + * + * @class wp.customize.Menus.MenuAutoAddControl + * @augments wp.customize.Control + */ + api.Menus.MenuAutoAddControl = api.Control.extend(/** @lends wp.customize.Menus.MenuAutoAddControl.prototype */{ + + ready: function() { + var control = this, + settingValue = control.setting(); + + /* + * Since the control is not registered in PHP, we need to prevent the + * preview's sending of the activeControls to result in this control + * being deactivated. + */ + control.active.validate = function() { + var value, section = api.section( control.section() ); + if ( section ) { + value = section.active(); + } else { + value = false; + } + return value; + }; + + control.autoAddElement = new api.Element( control.container.find( 'input[type=checkbox].auto_add' ) ); + + control.autoAddElement.bind(function( value ) { + var settingValue = control.setting(); + if ( settingValue && settingValue.name !== value ) { + settingValue = _.clone( settingValue ); + settingValue.auto_add = value; + control.setting.set( settingValue ); + } + }); + if ( settingValue ) { + control.autoAddElement.set( settingValue.auto_add ); + } + + control.setting.bind(function( object ) { + if ( object ) { + control.autoAddElement.set( object.auto_add ); + } + }); + } + + }); + + /** + * wp.customize.Menus.MenuControl + * + * Customizer control for menus. + * Note that 'nav_menu' must match the WP_Menu_Customize_Control::$type + * + * @class wp.customize.Menus.MenuControl + * @augments wp.customize.Control + */ + api.Menus.MenuControl = api.Control.extend(/** @lends wp.customize.Menus.MenuControl.prototype */{ + /** + * Set up the control. + */ + ready: function() { + var control = this, + section = api.section( control.section() ), + menuId = control.params.menu_id, + menu = control.setting(), + name, + widgetTemplate, + select; + + if ( 'undefined' === typeof this.params.menu_id ) { + throw new Error( 'params.menu_id was not defined' ); + } + + /* + * Since the control is not registered in PHP, we need to prevent the + * preview's sending of the activeControls to result in this control + * being deactivated. + */ + control.active.validate = function() { + var value; + if ( section ) { + value = section.active(); + } else { + value = false; + } + return value; + }; + + control.$controlSection = section.headContainer; + control.$sectionContent = control.container.closest( '.accordion-section-content' ); + + this._setupModel(); + + api.section( control.section(), function( section ) { + section.deferred.initSortables.done(function( menuList ) { + control._setupSortable( menuList ); + }); + } ); + + this._setupAddition(); + this._setupTitle(); + + // Add menu to Navigation Menu widgets. + if ( menu ) { + name = displayNavMenuName( menu.name ); + + // Add the menu to the existing controls. + api.control.each( function( widgetControl ) { + if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) { + return; + } + widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).show(); + widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).hide(); + + select = widgetControl.container.find( 'select' ); + if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) { + select.append( new Option( name, menuId ) ); + } + } ); + + // Add the menu to the widget template. + widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' ); + widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).show(); + widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).hide(); + select = widgetTemplate.find( '.widget-inside select:first' ); + if ( 0 === select.find( 'option[value=' + String( menuId ) + ']' ).length ) { + select.append( new Option( name, menuId ) ); + } + } + + /* + * Wait for menu items to be added. + * Ideally, we'd bind to an event indicating construction is complete, + * but deferring appears to be the best option today. + */ + _.defer( function () { + control.updateInvitationVisibility(); + } ); + }, + + /** + * Update ordering of menu item controls when the setting is updated. + */ + _setupModel: function() { + var control = this, + menuId = control.params.menu_id; + + control.setting.bind( function( to ) { + var name; + if ( false === to ) { + control._handleDeletion(); + } else { + // Update names in the Navigation Menu widgets. + name = displayNavMenuName( to.name ); + api.control.each( function( widgetControl ) { + if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) { + return; + } + var select = widgetControl.container.find( 'select' ); + select.find( 'option[value=' + String( menuId ) + ']' ).text( name ); + }); + } + } ); + }, + + /** + * Allow items in each menu to be re-ordered, and for the order to be previewed. + * + * Notice that the UI aspects here are handled by wpNavMenu.initSortables() + * which is called in MenuSection.onChangeExpanded() + * + * @param {Object} menuList - The element that has sortable(). + */ + _setupSortable: function( menuList ) { + var control = this; + + if ( ! menuList.is( control.$sectionContent ) ) { + throw new Error( 'Unexpected menuList.' ); + } + + menuList.on( 'sortstart', function() { + control.isSorting = true; + }); + + menuList.on( 'sortstop', function() { + setTimeout( function() { // Next tick. + var menuItemContainerIds = control.$sectionContent.sortable( 'toArray' ), + menuItemControls = [], + position = 0, + priority = 10; + + control.isSorting = false; + + // Reset horizontal scroll position when done dragging. + control.$sectionContent.scrollLeft( 0 ); + + _.each( menuItemContainerIds, function( menuItemContainerId ) { + var menuItemId, menuItemControl, matches; + matches = menuItemContainerId.match( /^customize-control-nav_menu_item-(-?\d+)$/, '' ); + if ( ! matches ) { + return; + } + menuItemId = parseInt( matches[1], 10 ); + menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' ); + if ( menuItemControl ) { + menuItemControls.push( menuItemControl ); + } + } ); + + _.each( menuItemControls, function( menuItemControl ) { + if ( false === menuItemControl.setting() ) { + // Skip deleted items. + return; + } + var setting = _.clone( menuItemControl.setting() ); + position += 1; + priority += 1; + setting.position = position; + menuItemControl.priority( priority ); + + // Note that wpNavMenu will be setting this .menu-item-data-parent-id input's value. + setting.menu_item_parent = parseInt( menuItemControl.container.find( '.menu-item-data-parent-id' ).val(), 10 ); + if ( ! setting.menu_item_parent ) { + setting.menu_item_parent = 0; + } + + menuItemControl.setting.set( setting ); + }); + }); + + }); + control.isReordering = false; + + /** + * Keyboard-accessible reordering. + */ + this.container.find( '.reorder-toggle' ).on( 'click', function() { + control.toggleReordering( ! control.isReordering ); + } ); + }, + + /** + * Set up UI for adding a new menu item. + */ + _setupAddition: function() { + var self = this; + + this.container.find( '.add-new-menu-item' ).on( 'click', function( event ) { + if ( self.$sectionContent.hasClass( 'reordering' ) ) { + return; + } + + if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) { + $( this ).attr( 'aria-expanded', 'true' ); + api.Menus.availableMenuItemsPanel.open( self ); + } else { + $( this ).attr( 'aria-expanded', 'false' ); + api.Menus.availableMenuItemsPanel.close(); + event.stopPropagation(); + } + } ); + }, + + _handleDeletion: function() { + var control = this, + section, + menuId = control.params.menu_id, + removeSection, + widgetTemplate, + navMenuCount = 0; + section = api.section( control.section() ); + removeSection = function() { + section.container.remove(); + api.section.remove( section.id ); + }; + + if ( section && section.expanded() ) { + section.collapse({ + completeCallback: function() { + removeSection(); + wp.a11y.speak( api.Menus.data.l10n.menuDeleted ); + api.panel( 'nav_menus' ).focus(); + } + }); + } else { + removeSection(); + } + + api.each(function( setting ) { + if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) { + navMenuCount += 1; + } + }); + + // Remove the menu from any Navigation Menu widgets. + api.control.each(function( widgetControl ) { + if ( ! widgetControl.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== widgetControl.params.widget_id_base ) { + return; + } + var select = widgetControl.container.find( 'select' ); + if ( select.val() === String( menuId ) ) { + select.prop( 'selectedIndex', 0 ).trigger( 'change' ); + } + + widgetControl.container.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount ); + widgetControl.container.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount ); + widgetControl.container.find( 'option[value=' + String( menuId ) + ']' ).remove(); + }); + + // Remove the menu to the nav menu widget template. + widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' ); + widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount ); + widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount ); + widgetTemplate.find( 'option[value=' + String( menuId ) + ']' ).remove(); + }, + + /** + * Update Section Title as menu name is changed. + */ + _setupTitle: function() { + var control = this; + + control.setting.bind( function( menu ) { + if ( ! menu ) { + return; + } + + var section = api.section( control.section() ), + menuId = control.params.menu_id, + controlTitle = section.headContainer.find( '.accordion-section-title' ), + sectionTitle = section.contentContainer.find( '.customize-section-title h3' ), + location = section.headContainer.find( '.menu-in-location' ), + action = sectionTitle.find( '.customize-action' ), + name = displayNavMenuName( menu.name ); + + // Update the control title. + controlTitle.text( name ); + if ( location.length ) { + location.appendTo( controlTitle ); + } + + // Update the section title. + sectionTitle.text( name ); + if ( action.length ) { + action.prependTo( sectionTitle ); + } + + // Update the nav menu name in location selects. + api.control.each( function( control ) { + if ( /^nav_menu_locations\[/.test( control.id ) ) { + control.container.find( 'option[value=' + menuId + ']' ).text( name ); + } + } ); + + // Update the nav menu name in all location checkboxes. + section.contentContainer.find( '.customize-control-checkbox input' ).each( function() { + if ( $( this ).prop( 'checked' ) ) { + $( '.current-menu-location-name-' + $( this ).data( 'location-id' ) ).text( name ); + } + } ); + } ); + }, + + /*********************************************************************** + * Begin public API methods + **********************************************************************/ + + /** + * Enable/disable the reordering UI + * + * @param {boolean} showOrHide to enable/disable reordering + */ + toggleReordering: function( showOrHide ) { + var addNewItemBtn = this.container.find( '.add-new-menu-item' ), + reorderBtn = this.container.find( '.reorder-toggle' ), + itemsTitle = this.$sectionContent.find( '.item-title' ); + + showOrHide = Boolean( showOrHide ); + + if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) { + return; + } + + this.isReordering = showOrHide; + this.$sectionContent.toggleClass( 'reordering', showOrHide ); + this.$sectionContent.sortable( this.isReordering ? 'disable' : 'enable' ); + if ( this.isReordering ) { + addNewItemBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); + reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOff ); + wp.a11y.speak( api.Menus.data.l10n.reorderModeOn ); + itemsTitle.attr( 'aria-hidden', 'false' ); + } else { + addNewItemBtn.removeAttr( 'tabindex aria-hidden' ); + reorderBtn.attr( 'aria-label', api.Menus.data.l10n.reorderLabelOn ); + wp.a11y.speak( api.Menus.data.l10n.reorderModeOff ); + itemsTitle.attr( 'aria-hidden', 'true' ); + } + + if ( showOrHide ) { + _( this.getMenuItemControls() ).each( function( formControl ) { + formControl.collapseForm(); + } ); + } + }, + + /** + * @return {wp.customize.controlConstructor.nav_menu_item[]} + */ + getMenuItemControls: function() { + var menuControl = this, + menuItemControls = [], + menuTermId = menuControl.params.menu_id; + + api.control.each(function( control ) { + if ( 'nav_menu_item' === control.params.type && control.setting() && menuTermId === control.setting().nav_menu_term_id ) { + menuItemControls.push( control ); + } + }); + + return menuItemControls; + }, + + /** + * Make sure that each menu item control has the proper depth. + */ + reflowMenuItems: function() { + var menuControl = this, + menuItemControls = menuControl.getMenuItemControls(), + reflowRecursively; + + reflowRecursively = function( context ) { + var currentMenuItemControls = [], + thisParent = context.currentParent; + _.each( context.menuItemControls, function( menuItemControl ) { + if ( thisParent === menuItemControl.setting().menu_item_parent ) { + currentMenuItemControls.push( menuItemControl ); + // @todo We could remove this item from menuItemControls now, for efficiency. + } + }); + currentMenuItemControls.sort( function( a, b ) { + return a.setting().position - b.setting().position; + }); + + _.each( currentMenuItemControls, function( menuItemControl ) { + // Update position. + context.currentAbsolutePosition += 1; + menuItemControl.priority.set( context.currentAbsolutePosition ); // This will change the sort order. + + // Update depth. + if ( ! menuItemControl.container.hasClass( 'menu-item-depth-' + String( context.currentDepth ) ) ) { + _.each( menuItemControl.container.prop( 'className' ).match( /menu-item-depth-\d+/g ), function( className ) { + menuItemControl.container.removeClass( className ); + }); + menuItemControl.container.addClass( 'menu-item-depth-' + String( context.currentDepth ) ); + } + menuItemControl.container.data( 'item-depth', context.currentDepth ); + + // Process any children items. + context.currentDepth += 1; + context.currentParent = menuItemControl.params.menu_item_id; + reflowRecursively( context ); + context.currentDepth -= 1; + context.currentParent = thisParent; + }); + + // Update class names for reordering controls. + if ( currentMenuItemControls.length ) { + _( currentMenuItemControls ).each(function( menuItemControl ) { + menuItemControl.container.removeClass( 'move-up-disabled move-down-disabled move-left-disabled move-right-disabled' ); + if ( 0 === context.currentDepth ) { + menuItemControl.container.addClass( 'move-left-disabled' ); + } else if ( 10 === context.currentDepth ) { + menuItemControl.container.addClass( 'move-right-disabled' ); + } + }); + + currentMenuItemControls[0].container + .addClass( 'move-up-disabled' ) + .addClass( 'move-right-disabled' ) + .toggleClass( 'move-down-disabled', 1 === currentMenuItemControls.length ); + currentMenuItemControls[ currentMenuItemControls.length - 1 ].container + .addClass( 'move-down-disabled' ) + .toggleClass( 'move-up-disabled', 1 === currentMenuItemControls.length ); + } + }; + + reflowRecursively( { + menuItemControls: menuItemControls, + currentParent: 0, + currentDepth: 0, + currentAbsolutePosition: 0 + } ); + + menuControl.updateInvitationVisibility( menuItemControls ); + menuControl.container.find( '.reorder-toggle' ).toggle( menuItemControls.length > 1 ); + }, + + /** + * Note that this function gets debounced so that when a lot of setting + * changes are made at once, for instance when moving a menu item that + * has child items, this function will only be called once all of the + * settings have been updated. + */ + debouncedReflowMenuItems: _.debounce( function() { + this.reflowMenuItems.apply( this, arguments ); + }, 0 ), + + /** + * Add a new item to this menu. + * + * @param {Object} item - Value for the nav_menu_item setting to be created. + * @return {wp.customize.Menus.controlConstructor.nav_menu_item} The newly-created nav_menu_item control instance. + */ + addItemToMenu: function( item ) { + var menuControl = this, customizeId, settingArgs, setting, menuItemControl, placeholderId, position = 0, priority = 10, + originalItemId = item.id || ''; + + _.each( menuControl.getMenuItemControls(), function( control ) { + if ( false === control.setting() ) { + return; + } + priority = Math.max( priority, control.priority() ); + if ( 0 === control.setting().menu_item_parent ) { + position = Math.max( position, control.setting().position ); + } + }); + position += 1; + priority += 1; + + item = $.extend( + {}, + api.Menus.data.defaultSettingValues.nav_menu_item, + item, + { + nav_menu_term_id: menuControl.params.menu_id, + original_title: item.title, + position: position + } + ); + delete item.id; // Only used by Backbone. + + placeholderId = api.Menus.generatePlaceholderAutoIncrementId(); + customizeId = 'nav_menu_item[' + String( placeholderId ) + ']'; + settingArgs = { + type: 'nav_menu_item', + transport: api.Menus.data.settingTransport, + previewer: api.previewer + }; + setting = api.create( customizeId, customizeId, {}, settingArgs ); + setting.set( item ); // Change from initial empty object to actual item to mark as dirty. + + // Add the menu item control. + menuItemControl = new api.controlConstructor.nav_menu_item( customizeId, { + type: 'nav_menu_item', + section: menuControl.id, + priority: priority, + settings: { + 'default': customizeId + }, + menu_item_id: placeholderId, + original_item_id: originalItemId + } ); + + api.control.add( menuItemControl ); + setting.preview(); + menuControl.debouncedReflowMenuItems(); + + wp.a11y.speak( api.Menus.data.l10n.itemAdded ); + + return menuItemControl; + }, + + /** + * Show an invitation to add new menu items when there are no menu items. + * + * @since 4.9.0 + * + * @param {wp.customize.controlConstructor.nav_menu_item[]} optionalMenuItemControls + */ + updateInvitationVisibility: function ( optionalMenuItemControls ) { + var menuItemControls = optionalMenuItemControls || this.getMenuItemControls(); + + this.container.find( '.new-menu-item-invitation' ).toggle( menuItemControls.length === 0 ); + } + } ); + + /** + * Extends wp.customize.controlConstructor with control constructor for + * menu_location, menu_item, nav_menu, and new_menu. + */ + $.extend( api.controlConstructor, { + nav_menu_location: api.Menus.MenuLocationControl, + nav_menu_item: api.Menus.MenuItemControl, + nav_menu: api.Menus.MenuControl, + nav_menu_name: api.Menus.MenuNameControl, + nav_menu_locations: api.Menus.MenuLocationsControl, + nav_menu_auto_add: api.Menus.MenuAutoAddControl + }); + + /** + * Extends wp.customize.panelConstructor with section constructor for menus. + */ + $.extend( api.panelConstructor, { + nav_menus: api.Menus.MenusPanel + }); + + /** + * Extends wp.customize.sectionConstructor with section constructor for menu. + */ + $.extend( api.sectionConstructor, { + nav_menu: api.Menus.MenuSection, + new_menu: api.Menus.NewMenuSection + }); + + /** + * Init Customizer for menus. + */ + api.bind( 'ready', function() { + + // Set up the menu items panel. + api.Menus.availableMenuItemsPanel = new api.Menus.AvailableMenuItemsPanelView({ + collection: api.Menus.availableMenuItems + }); + + api.bind( 'saved', function( data ) { + if ( data.nav_menu_updates || data.nav_menu_item_updates ) { + api.Menus.applySavedData( data ); + } + } ); + + /* + * Reset the list of posts created in the customizer once published. + * The setting is updated quietly (bypassing events being triggered) + * so that the customized state doesn't become immediately dirty. + */ + api.state( 'changesetStatus' ).bind( function( status ) { + if ( 'publish' === status ) { + api( 'nav_menus_created_posts' )._value = []; + } + } ); + + // Open and focus menu control. + api.previewer.bind( 'focus-nav-menu-item-control', api.Menus.focusMenuItemControl ); + } ); + + /** + * When customize_save comes back with a success, make sure any inserted + * nav menus and items are properly re-added with their newly-assigned IDs. + * + * @alias wp.customize.Menus.applySavedData + * + * @param {Object} data + * @param {Array} data.nav_menu_updates + * @param {Array} data.nav_menu_item_updates + */ + api.Menus.applySavedData = function( data ) { + + var insertedMenuIdMapping = {}, insertedMenuItemIdMapping = {}; + + _( data.nav_menu_updates ).each(function( update ) { + var oldCustomizeId, newCustomizeId, customizeId, oldSetting, newSetting, setting, settingValue, oldSection, newSection, wasSaved, widgetTemplate, navMenuCount, shouldExpandNewSection; + if ( 'inserted' === update.status ) { + if ( ! update.previous_term_id ) { + throw new Error( 'Expected previous_term_id' ); + } + if ( ! update.term_id ) { + throw new Error( 'Expected term_id' ); + } + oldCustomizeId = 'nav_menu[' + String( update.previous_term_id ) + ']'; + if ( ! api.has( oldCustomizeId ) ) { + throw new Error( 'Expected setting to exist: ' + oldCustomizeId ); + } + oldSetting = api( oldCustomizeId ); + if ( ! api.section.has( oldCustomizeId ) ) { + throw new Error( 'Expected control to exist: ' + oldCustomizeId ); + } + oldSection = api.section( oldCustomizeId ); + + settingValue = oldSetting.get(); + if ( ! settingValue ) { + throw new Error( 'Did not expect setting to be empty (deleted).' ); + } + settingValue = $.extend( _.clone( settingValue ), update.saved_value ); + + insertedMenuIdMapping[ update.previous_term_id ] = update.term_id; + newCustomizeId = 'nav_menu[' + String( update.term_id ) + ']'; + newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { + type: 'nav_menu', + transport: api.Menus.data.settingTransport, + previewer: api.previewer + } ); + + shouldExpandNewSection = oldSection.expanded(); + if ( shouldExpandNewSection ) { + oldSection.collapse(); + } + + // Add the menu section. + newSection = new api.Menus.MenuSection( newCustomizeId, { + panel: 'nav_menus', + title: settingValue.name, + customizeAction: api.Menus.data.l10n.customizingMenus, + type: 'nav_menu', + priority: oldSection.priority.get(), + menu_id: update.term_id + } ); + + // Add new control for the new menu. + api.section.add( newSection ); + + // Update the values for nav menus in Navigation Menu controls. + api.control.each( function( setting ) { + if ( ! setting.extended( api.controlConstructor.widget_form ) || 'nav_menu' !== setting.params.widget_id_base ) { + return; + } + var select, oldMenuOption, newMenuOption; + select = setting.container.find( 'select' ); + oldMenuOption = select.find( 'option[value=' + String( update.previous_term_id ) + ']' ); + newMenuOption = select.find( 'option[value=' + String( update.term_id ) + ']' ); + newMenuOption.prop( 'selected', oldMenuOption.prop( 'selected' ) ); + oldMenuOption.remove(); + } ); + + // Delete the old placeholder nav_menu. + oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set. + oldSetting.set( false ); + oldSetting.preview(); + newSetting.preview(); + oldSetting._dirty = false; + + // Remove nav_menu section. + oldSection.container.remove(); + api.section.remove( oldCustomizeId ); + + // Update the nav_menu widget to reflect removed placeholder menu. + navMenuCount = 0; + api.each(function( setting ) { + if ( /^nav_menu\[/.test( setting.id ) && false !== setting() ) { + navMenuCount += 1; + } + }); + widgetTemplate = $( '#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )' ); + widgetTemplate.find( '.nav-menu-widget-form-controls:first' ).toggle( 0 !== navMenuCount ); + widgetTemplate.find( '.nav-menu-widget-no-menus-message:first' ).toggle( 0 === navMenuCount ); + widgetTemplate.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove(); + + // Update the nav_menu_locations[...] controls to remove the placeholder menus from the dropdown options. + wp.customize.control.each(function( control ){ + if ( /^nav_menu_locations\[/.test( control.id ) ) { + control.container.find( 'option[value=' + String( update.previous_term_id ) + ']' ).remove(); + } + }); + + // Update nav_menu_locations to reference the new ID. + api.each( function( setting ) { + var wasSaved = api.state( 'saved' ).get(); + if ( /^nav_menu_locations\[/.test( setting.id ) && setting.get() === update.previous_term_id ) { + setting.set( update.term_id ); + setting._dirty = false; // Not dirty because this is has also just been done on server in WP_Customize_Nav_Menu_Setting::update(). + api.state( 'saved' ).set( wasSaved ); + setting.preview(); + } + } ); + + if ( shouldExpandNewSection ) { + newSection.expand(); + } + } else if ( 'updated' === update.status ) { + customizeId = 'nav_menu[' + String( update.term_id ) + ']'; + if ( ! api.has( customizeId ) ) { + throw new Error( 'Expected setting to exist: ' + customizeId ); + } + + // Make sure the setting gets updated with its sanitized server value (specifically the conflict-resolved name). + setting = api( customizeId ); + if ( ! _.isEqual( update.saved_value, setting.get() ) ) { + wasSaved = api.state( 'saved' ).get(); + setting.set( update.saved_value ); + setting._dirty = false; + api.state( 'saved' ).set( wasSaved ); + } + } + } ); + + // Build up mapping of nav_menu_item placeholder IDs to inserted IDs. + _( data.nav_menu_item_updates ).each(function( update ) { + if ( update.previous_post_id ) { + insertedMenuItemIdMapping[ update.previous_post_id ] = update.post_id; + } + }); + + _( data.nav_menu_item_updates ).each(function( update ) { + var oldCustomizeId, newCustomizeId, oldSetting, newSetting, settingValue, oldControl, newControl; + if ( 'inserted' === update.status ) { + if ( ! update.previous_post_id ) { + throw new Error( 'Expected previous_post_id' ); + } + if ( ! update.post_id ) { + throw new Error( 'Expected post_id' ); + } + oldCustomizeId = 'nav_menu_item[' + String( update.previous_post_id ) + ']'; + if ( ! api.has( oldCustomizeId ) ) { + throw new Error( 'Expected setting to exist: ' + oldCustomizeId ); + } + oldSetting = api( oldCustomizeId ); + if ( ! api.control.has( oldCustomizeId ) ) { + throw new Error( 'Expected control to exist: ' + oldCustomizeId ); + } + oldControl = api.control( oldCustomizeId ); + + settingValue = oldSetting.get(); + if ( ! settingValue ) { + throw new Error( 'Did not expect setting to be empty (deleted).' ); + } + settingValue = _.clone( settingValue ); + + // If the parent menu item was also inserted, update the menu_item_parent to the new ID. + if ( settingValue.menu_item_parent < 0 ) { + if ( ! insertedMenuItemIdMapping[ settingValue.menu_item_parent ] ) { + throw new Error( 'inserted ID for menu_item_parent not available' ); + } + settingValue.menu_item_parent = insertedMenuItemIdMapping[ settingValue.menu_item_parent ]; + } + + // If the menu was also inserted, then make sure it uses the new menu ID for nav_menu_term_id. + if ( insertedMenuIdMapping[ settingValue.nav_menu_term_id ] ) { + settingValue.nav_menu_term_id = insertedMenuIdMapping[ settingValue.nav_menu_term_id ]; + } + + newCustomizeId = 'nav_menu_item[' + String( update.post_id ) + ']'; + newSetting = api.create( newCustomizeId, newCustomizeId, settingValue, { + type: 'nav_menu_item', + transport: api.Menus.data.settingTransport, + previewer: api.previewer + } ); + + // Add the menu control. + newControl = new api.controlConstructor.nav_menu_item( newCustomizeId, { + type: 'nav_menu_item', + menu_id: update.post_id, + section: 'nav_menu[' + String( settingValue.nav_menu_term_id ) + ']', + priority: oldControl.priority.get(), + settings: { + 'default': newCustomizeId + }, + menu_item_id: update.post_id + } ); + + // Remove old control. + oldControl.container.remove(); + api.control.remove( oldCustomizeId ); + + // Add new control to take its place. + api.control.add( newControl ); + + // Delete the placeholder and preview the new setting. + oldSetting.callbacks.disable(); // Prevent setting triggering Customizer dirty state when set. + oldSetting.set( false ); + oldSetting.preview(); + newSetting.preview(); + oldSetting._dirty = false; + + newControl.container.toggleClass( 'menu-item-edit-inactive', oldControl.container.hasClass( 'menu-item-edit-inactive' ) ); + } + }); + + /* + * Update the settings for any nav_menu widgets that had selected a placeholder ID. + */ + _.each( data.widget_nav_menu_updates, function( widgetSettingValue, widgetSettingId ) { + var setting = api( widgetSettingId ); + if ( setting ) { + setting._value = widgetSettingValue; + setting.preview(); // Send to the preview now so that menu refresh will use the inserted menu. + } + }); + }; + + /** + * Focus a menu item control. + * + * @alias wp.customize.Menus.focusMenuItemControl + * + * @param {string} menuItemId + */ + api.Menus.focusMenuItemControl = function( menuItemId ) { + var control = api.Menus.getMenuItemControl( menuItemId ); + if ( control ) { + control.focus(); + } + }; + + /** + * Get the control for a given menu. + * + * @alias wp.customize.Menus.getMenuControl + * + * @param menuId + * @return {wp.customize.controlConstructor.menus[]} + */ + api.Menus.getMenuControl = function( menuId ) { + return api.control( 'nav_menu[' + menuId + ']' ); + }; + + /** + * Given a menu item ID, get the control associated with it. + * + * @alias wp.customize.Menus.getMenuItemControl + * + * @param {string} menuItemId + * @return {Object|null} + */ + api.Menus.getMenuItemControl = function( menuItemId ) { + return api.control( menuItemIdToSettingId( menuItemId ) ); + }; + + /** + * @alias wp.customize.Menus~menuItemIdToSettingId + * + * @param {string} menuItemId + */ + function menuItemIdToSettingId( menuItemId ) { + return 'nav_menu_item[' + menuItemId + ']'; + } + + /** + * Apply sanitize_text_field()-like logic to the supplied name, returning a + * "unnammed" fallback string if the name is then empty. + * + * @alias wp.customize.Menus~displayNavMenuName + * + * @param {string} name + * @return {string} + */ + function displayNavMenuName( name ) { + name = name || ''; + name = wp.sanitize.stripTagsAndEncodeText( name ); // Remove any potential tags from name. + name = name.toString().trim(); + return name || api.Menus.data.l10n.unnamed; + } + +})( wp.customize, wp, jQuery ); diff --git a/wp-admin/js/customize-nav-menus.min.js b/wp-admin/js/customize-nav-menus.min.js new file mode 100644 index 0000000..bf20d50 --- /dev/null +++ b/wp-admin/js/customize-nav-menus.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(c,l,m){"use strict";function u(e){return(e=(e=l.sanitize.stripTagsAndEncodeText(e=e||"")).toString().trim())||c.Menus.data.l10n.unnamed}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()},c.Menus=c.Menus||{},c.Menus.data={itemTypes:[],l10n:{},settingTransport:"refresh",phpIntMax:0,defaultSettingValues:{nav_menu:{},nav_menu_item:{}},locationSlugMappedToName:{}},"undefined"!=typeof _wpCustomizeNavMenusSettings&&m.extend(c.Menus.data,_wpCustomizeNavMenusSettings),c.Menus.generatePlaceholderAutoIncrementId=function(){return-Math.ceil(c.Menus.data.phpIntMax*Math.random())},c.Menus.AvailableItemModel=Backbone.Model.extend(m.extend({id:null},c.Menus.data.defaultSettingValues.nav_menu_item)),c.Menus.AvailableItemCollection=Backbone.Collection.extend({model:c.Menus.AvailableItemModel,sort_key:"order",comparator:function(e){return-e.get(this.sort_key)},sortByField:function(e){this.sort_key=e,this.sort()}}),c.Menus.availableMenuItems=new c.Menus.AvailableItemCollection(c.Menus.data.availableMenuItems),c.Menus.insertAutoDraftPost=function(n){var i=m.Deferred(),e=l.ajax.post("customize-nav-menus-insert-auto-draft",{"customize-menus-nonce":c.settings.nonce["customize-menus"],wp_customize:"on",customize_changeset_uuid:c.settings.changeset.uuid,params:n});return e.done(function(t){t.post_id&&(c("nav_menus_created_posts").set(c("nav_menus_created_posts").get().concat([t.post_id])),"page"===n.post_type&&(c.section.has("static_front_page")&&c.section("static_front_page").activate(),c.control.each(function(e){"dropdown-pages"===e.params.type&&e.container.find('select[name^="_customize-dropdown-pages-"]').append(new Option(n.post_title,t.post_id))})),i.resolve(t))}),e.fail(function(e){var t=e||"";void 0!==e.message&&(t=e.message),console.error(t),i.rejectWith(t)}),i.promise()},c.Menus.AvailableMenuItemsPanelView=l.Backbone.View.extend({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"},selected:null,currentMenuControl:null,debounceSearch:null,$search:null,$clearResults:null,searchTerm:"",rendered:!1,pages:{},sectionContent:"",loading:!1,addingNew:!1,initialize:function(){var n=this;c.panel.has("nav_menus")&&(this.$search=m("#menu-items-search"),this.$clearResults=this.$el.find(".clear-results"),this.sectionContent=this.$el.find(".available-menu-items-list"),this.debounceSearch=_.debounce(n.search,500),_.bindAll(this,"close"),m("#customize-controls, .customize-section-back").on("click keydown",function(e){var t=m(e.target).is(".item-delete, .item-delete *"),e=m(e.target).is(".add-new-menu-item, .add-new-menu-item *");!m("body").hasClass("adding-menu-items")||t||e||n.close()}),this.$clearResults.on("click",function(){n.$search.val("").trigger("focus").trigger("input")}),this.$el.on("input","#custom-menu-item-name.invalid, #custom-menu-item-url.invalid",function(){m(this).removeClass("invalid")}),c.panel("nav_menus").container.on("expanded",function(){n.rendered||(n.initList(),n.rendered=!0)}),this.sectionContent.on("scroll",function(){var e=n.$el.find(".accordion-section.open .available-menu-items-list").prop("scrollHeight"),t=n.$el.find(".accordion-section.open").height();!n.loading&&m(this).scrollTop()>.75*e-t&&(e=m(this).data("type"),t=m(this).data("object"),"search"===e?n.searchTerm&&n.doSearch(n.pages.search):n.loadItems([{type:e,object:t}]))}),c.previewer.bind("url",this.close),n.delegateEvents())},search:function(e){var t=m("#available-menu-items-search"),n=m("#available-menu-items .accordion-section").not(t);e&&this.searchTerm!==e.target.value&&(""===e.target.value||t.hasClass("open")?""===e.target.value&&(t.removeClass("open"),n.show(),this.$clearResults.removeClass("is-visible")):(n.fadeOut(100),t.find(".accordion-section-content").slideDown("fast"),t.addClass("open"),this.$clearResults.addClass("is-visible")),this.searchTerm=e.target.value,this.pages.search=1,this.doSearch(1))},doSearch:function(t){var e,n=this,i=m("#available-menu-items-search"),a=i.find(".accordion-section-content"),o=l.template("available-menu-item");if(n.currentRequest&&n.currentRequest.abort(),!(t<0)){if(1<t)i.addClass("loading-more"),a.attr("aria-busy","true"),l.a11y.speak(c.Menus.data.l10n.itemsLoadingMore);else if(""===n.searchTerm)return a.html(""),void l.a11y.speak("");i.addClass("loading"),n.loading=!0,e=c.previewer.query({excludeCustomizedSaved:!0}),_.extend(e,{"customize-menus-nonce":c.settings.nonce["customize-menus"],wp_customize:"on",search:n.searchTerm,page:t}),n.currentRequest=l.ajax.post("search-available-menu-items-customizer",e),n.currentRequest.done(function(e){1===t&&a.empty(),i.removeClass("loading loading-more"),a.attr("aria-busy","false"),i.addClass("open"),n.loading=!1,e=new c.Menus.AvailableItemCollection(e.items),n.collection.add(e.models),e.each(function(e){a.append(o(e.attributes))}),e.length<20?n.pages.search=-1:n.pages.search=n.pages.search+1,e&&1<t?l.a11y.speak(c.Menus.data.l10n.itemsFoundMore.replace("%d",e.length)):e&&1===t&&l.a11y.speak(c.Menus.data.l10n.itemsFound.replace("%d",e.length))}),n.currentRequest.fail(function(e){e.message&&(a.empty().append(m('<li class="nothing-found"></li>').text(e.message)),l.a11y.speak(e.message)),n.pages.search=-1}),n.currentRequest.always(function(){i.removeClass("loading loading-more"),a.attr("aria-busy","false"),n.loading=!1,n.currentRequest=null})}},initList:function(){var t=this;_.each(c.Menus.data.itemTypes,function(e){t.pages[e.type+":"+e.object]=0}),t.loadItems(c.Menus.data.itemTypes)},loadItems:function(e,t){var i=this,a=[],o={},s=l.template("available-menu-item"),t=_.isString(e)&&_.isString(t)?[{type:e,object:t}]:e;_.each(t,function(e){var t,n=e.type+":"+e.object;-1!==i.pages[n]&&((t=m("#available-menu-items-"+e.type+"-"+e.object)).find(".accordion-section-title").addClass("loading"),o[n]=t,a.push({object:e.object,type:e.type,page:i.pages[n]}))}),0!==a.length&&(i.loading=!0,e=c.previewer.query({excludeCustomizedSaved:!0}),_.extend(e,{"customize-menus-nonce":c.settings.nonce["customize-menus"],wp_customize:"on",item_types:a}),(t=l.ajax.post("load-available-menu-items-customizer",e)).done(function(e){var n;_.each(e.items,function(e,t){0===e.length?(0===i.pages[t]&&o[t].find(".accordion-section-title").addClass("cannot-expand").removeClass("loading").find(".accordion-section-title > button").prop("tabIndex",-1),i.pages[t]=-1):("post_type:page"!==t||o[t].hasClass("open")||o[t].find(".accordion-section-title > button").trigger("click"),e=new c.Menus.AvailableItemCollection(e),i.collection.add(e.models),n=o[t].find(".available-menu-items-list"),e.each(function(e){n.append(s(e.attributes))}),i.pages[t]+=1)})}),t.fail(function(e){"undefined"!=typeof console&&console.error&&console.error(e)}),t.always(function(){_.each(o,function(e){e.find(".accordion-section-title").removeClass("loading")}),i.loading=!1}))},itemSectionHeight:function(){var e=window.innerHeight,t=this.$el.find(".accordion-section:not( #available-menu-items-search ) .accordion-section-content"),n=this.$el.find('.accordion-section:not( #available-menu-items-search ) .available-menu-items-list:not(":only-child")'),e=e-(46*(1+t.length)+14);120<e&&e<290&&(t.css("max-height",e),n.css("max-height",e-60))},select:function(e){this.selected=m(e),this.selected.siblings(".menu-item-tpl").removeClass("selected"),this.selected.addClass("selected")},focus:function(e){this.select(m(e.currentTarget))},_submit:function(e){"keypress"===e.type&&13!==e.which&&32!==e.which||this.submit(m(e.currentTarget))},submit:function(e){var t;(e=e||this.selected)&&this.currentMenuControl&&(this.select(e),t=m(this.selected).data("menu-item-id"),t=this.collection.findWhere({id:t}))&&(this.currentMenuControl.addItemToMenu(t.attributes),m(e).find(".menu-item-handle").addClass("item-added"))},_submitLink:function(e){"keypress"===e.type&&13!==e.which||this.submitLink()},submitLink:function(){var e,t=m("#custom-menu-item-name"),n=m("#custom-menu-item-url"),i=n.val().trim();this.currentMenuControl&&(e=/^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/,""===t.val()?t.addClass("invalid"):e.test(i)?(e={title:t.val(),url:i,type:"custom",type_label:c.Menus.data.l10n.custom_label,object:"custom"},this.currentMenuControl.addItemToMenu(e),n.val("").attr("placeholder","https://"),t.val("")):n.addClass("invalid"))},_submitNew:function(e){"keypress"===e.type&&13!==e.which||this.addingNew||(e=m(e.target).closest(".accordion-section"),this.submitNew(e))},submitNew:function(n){var i=this,a=n.find(".create-item-input"),e=a.val(),t=n.find(".available-menu-items-list"),o=t.data("type"),s=t.data("object"),r=t.data("type_label");this.currentMenuControl&&"post_type"===o&&(""===a.val().trim()?(a.addClass("invalid"),a.focus()):(a.removeClass("invalid"),n.find(".accordion-section-title").addClass("loading"),i.addingNew=!0,a.attr("disabled","disabled"),c.Menus.insertAutoDraftPost({post_title:e,post_type:s}).done(function(e){var t,e=new c.Menus.AvailableItemModel({id:"post-"+e.post_id,title:a.val(),type:o,type_label:r,object:s,object_id:e.post_id,url:e.url});i.currentMenuControl.addItemToMenu(e.attributes),c.Menus.availableMenuItemsPanel.collection.add(e),t=n.find(".available-menu-items-list"),(e=m(l.template("available-menu-item")(e.attributes))).find(".menu-item-handle:first").addClass("item-added"),t.prepend(e),t.scrollTop(),a.val("").removeAttr("disabled"),i.addingNew=!1,n.find(".accordion-section-title").removeClass("loading")})))},open:function(e){var t,n=this;this.currentMenuControl=e,this.itemSectionHeight(),c.section.has("publish_settings")&&c.section("publish_settings").collapse(),m("body").addClass("adding-menu-items"),t=function(){n.close(),m(this).off("click",t)},m("#customize-preview").on("click",t),_(this.currentMenuControl.getMenuItemControls()).each(function(e){e.collapseForm()}),this.$el.find(".selected").removeClass("selected"),this.$search.trigger("focus")},close:function(e){(e=e||{}).returnFocus&&this.currentMenuControl&&this.currentMenuControl.container.find(".add-new-menu-item").focus(),this.currentMenuControl=null,this.selected=null,m("body").removeClass("adding-menu-items"),m("#available-menu-items .menu-item-handle.item-added").removeClass("item-added"),this.$search.val("").trigger("input")},keyboardAccessible:function(e){var t=13===e.which,n=27===e.which,i=9===e.which&&e.shiftKey,a=m(e.target).is(this.$search);t&&!this.$search.val()||(a&&i?(this.currentMenuControl.container.find(".add-new-menu-item").focus(),e.preventDefault()):n&&this.close({returnFocus:!0}))}}),c.Menus.MenusPanel=c.Panel.extend({attachEvents:function(){c.Panel.prototype.attachEvents.call(this);var t=this.container.find(".panel-meta"),n=t.find(".customize-help-toggle"),i=t.find(".customize-panel-description"),a=m("#screen-options-wrap"),o=t.find(".customize-screen-options-toggle");o.on("click keydown",function(e){if(!c.utils.isKeydownButNotEnterEvent(e))return e.preventDefault(),i.not(":hidden")&&(i.slideUp("fast"),n.attr("aria-expanded","false")),"true"===o.attr("aria-expanded")?(o.attr("aria-expanded","false"),t.removeClass("open"),t.removeClass("active-menu-screen-options"),a.slideUp("fast")):(o.attr("aria-expanded","true"),t.addClass("open"),t.addClass("active-menu-screen-options"),a.slideDown("fast")),!1}),n.on("click keydown",function(e){c.utils.isKeydownButNotEnterEvent(e)||(e.preventDefault(),"true"===o.attr("aria-expanded")&&(o.attr("aria-expanded","false"),n.attr("aria-expanded","true"),t.addClass("open"),t.removeClass("active-menu-screen-options"),a.slideUp("fast"),i.slideDown("fast")))})},ready:function(){var e=this;e.container.find(".hide-column-tog").on("click",function(){e.saveManageColumnsState()}),c.section("menu_locations",function(e){e.headContainer.prepend(l.template("nav-menu-locations-header")(c.Menus.data))})},saveManageColumnsState:_.debounce(function(){var e=this;e._updateHiddenColumnsRequest&&e._updateHiddenColumnsRequest.abort(),e._updateHiddenColumnsRequest=l.ajax.post("hidden-columns",{hidden:e.hidden(),screenoptionnonce:m("#screenoptionnonce").val(),page:"nav-menus"}),e._updateHiddenColumnsRequest.always(function(){e._updateHiddenColumnsRequest=null})},2e3),checked:function(){},unchecked:function(){},hidden:function(){return m(".hide-column-tog").not(":checked").map(function(){var e=this.id;return e.substring(0,e.length-5)}).get().join(",")}}),c.Menus.MenuSection=c.Section.extend({initialize:function(e,t){c.Section.prototype.initialize.call(this,e,t),this.deferred.initSortables=m.Deferred()},ready:function(){var e,t,n=this;if(void 0===n.params.menu_id)throw new Error("params.menu_id was not defined");n.active.validate=function(){return!!c.has(n.id)&&!!c(n.id).get()},n.populateControls(),n.navMenuLocationSettings={},n.assignedLocations=new c.Value([]),c.each(function(e,t){t=t.match(/^nav_menu_locations\[(.+?)]/);t&&(n.navMenuLocationSettings[t[1]]=e).bind(function(){n.refreshAssignedLocations()})}),n.assignedLocations.bind(function(e){n.updateAssignedLocationsInSectionTitle(e)}),n.refreshAssignedLocations(),c.bind("pane-contents-reflowed",function(){n.contentContainer.parent().length&&(n.container.find(".menu-item .menu-item-reorder-nav button").attr({tabindex:"0","aria-hidden":"false"}),n.container.find(".menu-item.move-up-disabled .menus-move-up").attr({tabindex:"-1","aria-hidden":"true"}),n.container.find(".menu-item.move-down-disabled .menus-move-down").attr({tabindex:"-1","aria-hidden":"true"}),n.container.find(".menu-item.move-left-disabled .menus-move-left").attr({tabindex:"-1","aria-hidden":"true"}),n.container.find(".menu-item.move-right-disabled .menus-move-right").attr({tabindex:"-1","aria-hidden":"true"}))}),t=function(){var e="field-"+m(this).val()+"-active";n.contentContainer.toggleClass(e,m(this).prop("checked"))},(e=c.panel("nav_menus").contentContainer.find(".metabox-prefs:first").find(".hide-column-tog")).each(t),e.on("click",t)},populateControls:function(){var e,t=this,n=t.id+"[name]",i=c.control(n);i||(i=new c.controlConstructor.nav_menu_name(n,{type:"nav_menu_name",label:c.Menus.data.l10n.menuNameLabel,section:t.id,priority:0,settings:{default:t.id}}),c.control.add(i),i.active.set(!0)),(n=c.control(t.id))||(n=new c.controlConstructor.nav_menu(t.id,{type:"nav_menu",section:t.id,priority:998,settings:{default:t.id},menu_id:t.params.menu_id}),c.control.add(n),n.active.set(!0)),i=t.id+"[locations]",c.control(i)||(i=new c.controlConstructor.nav_menu_locations(i,{section:t.id,priority:999,settings:{default:t.id},menu_id:t.params.menu_id}),c.control.add(i.id,i),n.active.set(!0)),i=t.id+"[auto_add]",(n=c.control(i))||(n=new c.controlConstructor.nav_menu_auto_add(i,{type:"nav_menu_auto_add",label:"",section:t.id,priority:1e3,settings:{default:t.id}}),c.control.add(n),n.active.set(!0)),i=t.id+"[delete]",(e=c.control(i))||(e=new c.Control(i,{section:t.id,priority:1001,templateId:"nav-menu-delete-button"}),c.control.add(e.id,e),e.active.set(!0),e.deferred.embedded.done(function(){e.container.find("button").on("click",function(){var e=t.params.menu_id;c.Menus.getMenuControl(e).setting.set(!1)})}))},refreshAssignedLocations:function(){var n=this.params.menu_id,i=[];_.each(this.navMenuLocationSettings,function(e,t){e()===n&&i.push(t)}),this.assignedLocations.set(i)},updateAssignedLocationsInSectionTitle:function(e){var n=this.container.find(".accordion-section-title:first");n.find(".menu-in-location").remove(),_.each(e,function(e){var t=m('<span class="menu-in-location"></span>'),e=c.Menus.data.locationSlugMappedToName[e];t.text(c.Menus.data.l10n.menuLocation.replace("%s",e)),n.append(t)}),this.container.toggleClass("assigned-to-menu-location",0!==e.length)},onChangeExpanded:function(e,t){var n,i=this;e&&(wpNavMenu.menuList=i.contentContainer,wpNavMenu.targetList=wpNavMenu.menuList,m("#menu-to-edit").removeAttr("id"),wpNavMenu.menuList.attr("id","menu-to-edit").addClass("menu"),_.each(c.section(i.id).controls(),function(e){"nav_menu_item"===e.params.type&&e.actuallyEmbed()}),t.completeCallback&&(n=t.completeCallback),t.completeCallback=function(){"resolved"!==i.deferred.initSortables.state()&&(wpNavMenu.initSortables(),i.deferred.initSortables.resolve(wpNavMenu.menuList),c.control("nav_menu["+String(i.params.menu_id)+"]").reflowMenuItems()),_.isFunction(n)&&n()}),c.Section.prototype.onChangeExpanded.call(i,e,t)},highlightNewItemButton:function(){c.utils.highlightButton(this.contentContainer.find(".add-new-menu-item"),{delay:2e3})}}),c.Menus.createNavMenu=function(e){var t=c.Menus.generatePlaceholderAutoIncrementId(),n="nav_menu["+String(t)+"]";return c.create(n,n,{},{type:"nav_menu",transport:c.Menus.data.settingTransport,previewer:c.previewer}).set(m.extend({},c.Menus.data.defaultSettingValues.nav_menu,{name:e||""})),c.section.add(new c.Menus.MenuSection(n,{panel:"nav_menus",title:u(e),customizeAction:c.Menus.data.l10n.customizingMenus,priority:10,menu_id:t}))},c.Menus.NewMenuSection=c.Section.extend({attachEvents:function(){var t=this,e=t.container,n=t.contentContainer,i=/^nav_menu\[/;function a(){var t;e.find(".add-new-menu-notice").prop("hidden",(t=0,c.each(function(e){i.test(e.id)&&!1!==e.get()&&(t+=1)}),0<t))}function o(e){i.test(e.id)&&(e.bind(a),a())}t.headContainer.find(".accordion-section-title").replaceWith(l.template("nav-menu-create-menu-section-title")),e.on("click",".customize-add-menu-button",function(){t.expand()}),n.on("keydown",".menu-name-field",function(e){13===e.which&&t.submit()}),n.on("click","#customize-new-menu-submit",function(e){t.submit(),e.stopPropagation(),e.preventDefault()}),c.each(o),c.bind("add",o),c.bind("removed",function(e){i.test(e.id)&&(e.unbind(a),a())}),a(),c.Section.prototype.attachEvents.apply(t,arguments)},ready:function(){this.populateControls()},populateControls:function(){var e=this,t=e.id+"[name]",n=c.control(t);n||(n=new c.controlConstructor.nav_menu_name(t,{label:c.Menus.data.l10n.menuNameLabel,description:c.Menus.data.l10n.newMenuNameDescription,section:e.id,priority:0}),c.control.add(n.id,n),n.active.set(!0)),t=e.id+"[locations]",(n=c.control(t))||(n=new c.controlConstructor.nav_menu_locations(t,{section:e.id,priority:1,menu_id:"",isCreating:!0}),c.control.add(t,n),n.active.set(!0)),t=e.id+"[submit]",(n=c.control(t))||(n=new c.Control(t,{section:e.id,priority:1,templateId:"nav-menu-submit-new-button"}),c.control.add(t,n),n.active.set(!0))},submit:function(){var t,e=this.contentContainer,n=e.find(".menu-name-field").first(),i=n.val();i?(t=c.Menus.createNavMenu(i),n.val(""),n.removeClass("invalid"),e.find(".assigned-menu-location input[type=checkbox]").each(function(){var e=m(this);e.prop("checked")&&(c("nav_menu_locations["+e.data("location-id")+"]").set(t.params.menu_id),e.prop("checked",!1))}),l.a11y.speak(c.Menus.data.l10n.menuAdded),t.focus({completeCallback:function(){t.highlightNewItemButton()}})):(n.addClass("invalid"),n.focus())},selectDefaultLocation:function(e){var t=c.control(this.id+"[locations]"),n={};null!==e&&(n[e]=!0),t.setSelections(n)}}),c.Menus.MenuLocationControl=c.Control.extend({initialize:function(e,t){var n=e.match(/^nav_menu_locations\[(.+?)]/);this.themeLocation=n[1],c.Control.prototype.initialize.call(this,e,t)},ready:function(){var n=this,i=/^nav_menu\[(-?\d+)]/;n.setting.validate=function(e){return""===e?0:parseInt(e,10)},n.container.find(".create-menu").on("click",function(){var e=c.section("add_menu");e.selectDefaultLocation(this.dataset.locationId),e.focus()}),n.container.find(".edit-menu").on("click",function(){var e=n.setting();c.section("nav_menu["+e+"]").focus()}),n.setting.bind("change",function(){var e=0!==n.setting();n.container.find(".create-menu").toggleClass("hidden",e),n.container.find(".edit-menu").toggleClass("hidden",!e)}),c.bind("add",function(e){var t=e.id.match(i);t&&!1!==e()&&(t=t[1],e=new Option(u(e().name),t),n.container.find("select").append(e))}),c.bind("remove",function(e){var e=e.id.match(i);e&&(e=parseInt(e[1],10),n.setting()===e&&n.setting.set(""),n.container.find("option[value="+e+"]").remove())}),c.bind("change",function(e){var t=e.id.match(i);t&&(t=parseInt(t[1],10),!1===e()?(n.setting()===t&&n.setting.set(""),n.container.find("option[value="+t+"]").remove()):n.container.find("option[value="+t+"]").text(u(e().name)))})}}),c.Menus.MenuItemControl=c.Control.extend({initialize:function(e,t){var n=this;n.expanded=new c.Value(!1),n.expandedArgumentsQueue=[],n.expanded.bind(function(e){var t=n.expandedArgumentsQueue.shift(),t=m.extend({},n.defaultExpandedArguments,t);n.onChangeExpanded(e,t)}),c.Control.prototype.initialize.call(n,e,t),n.active.validate=function(){var e=c.section(n.section()),e=!!e&&e.active();return e}},embed:function(){var e=this.section();e&&((e=c.section(e))&&e.expanded()||c.settings.autofocus.control===this.id)&&this.actuallyEmbed()},actuallyEmbed:function(){"resolved"!==this.deferred.embedded.state()&&(this.renderContent(),this.deferred.embedded.resolve())},ready:function(){if(void 0===this.params.menu_item_id)throw new Error("params.menu_item_id was not defined");this._setupControlToggle(),this._setupReorderUI(),this._setupUpdateUI(),this._setupRemoveUI(),this._setupLinksUI(),this._setupTitleUI()},_setupControlToggle:function(){var i=this;this.container.find(".menu-item-handle").on("click",function(e){e.preventDefault(),e.stopPropagation();var t=i.getMenuControl(),n=m(e.target).is(".item-delete, .item-delete *"),e=m(e.target).is(".add-new-menu-item, .add-new-menu-item *");!m("body").hasClass("adding-menu-items")||n||e||c.Menus.availableMenuItemsPanel.close(),t.isReordering||t.isSorting||i.toggleForm()})},_setupReorderUI:function(){var o=this,e=l.template("menu-item-reorder-nav");o.container.find(".item-controls").after(e),o.container.find(".menu-item-reorder-nav").find(".menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right").on("click",function(){var e=m(this),t=(e.focus(),e.is(".menus-move-up")),n=e.is(".menus-move-down"),i=e.is(".menus-move-left"),a=e.is(".menus-move-right");t?o.moveUp():n?o.moveDown():i?o.moveLeft():a&&o.moveRight(),e.focus()})},_setupUpdateUI:function(){var e,s=this,t=s.setting();s.elements={},s.elements.url=new c.Element(s.container.find(".edit-menu-item-url")),s.elements.title=new c.Element(s.container.find(".edit-menu-item-title")),s.elements.attr_title=new c.Element(s.container.find(".edit-menu-item-attr-title")),s.elements.target=new c.Element(s.container.find(".edit-menu-item-target")),s.elements.classes=new c.Element(s.container.find(".edit-menu-item-classes")),s.elements.xfn=new c.Element(s.container.find(".edit-menu-item-xfn")),s.elements.description=new c.Element(s.container.find(".edit-menu-item-description")),_.each(s.elements,function(n,i){n.bind(function(e){n.element.is("input[type=checkbox]")&&(e=e?n.element.val():"");var t=s.setting();t&&t[i]!==e&&((t=_.clone(t))[i]=e,s.setting.set(t))}),t&&("classes"!==i&&"xfn"!==i||!_.isArray(t[i])?n.set(t[i]):n.set(t[i].join(" ")))}),s.setting.bind(function(n,i){var e,t=s.params.menu_item_id,a=[],o=[];!1===n?(e=c.control("nav_menu["+String(i.nav_menu_term_id)+"]"),s.container.remove(),_.each(e.getMenuItemControls(),function(e){i.menu_item_parent===e.setting().menu_item_parent&&e.setting().position>i.position?a.push(e):e.setting().menu_item_parent===t&&o.push(e)}),_.each(a,function(e){var t=_.clone(e.setting());t.position+=o.length,e.setting.set(t)}),_.each(o,function(e,t){var n=_.clone(e.setting());n.position=i.position+t,n.menu_item_parent=i.menu_item_parent,e.setting.set(n)}),e.debouncedReflowMenuItems()):(_.each(n,function(e,t){s.elements[t]&&s.elements[t].set(n[t])}),s.container.find(".menu-item-data-parent-id").val(n.menu_item_parent),n.position===i.position&&n.menu_item_parent===i.menu_item_parent||s.getMenuControl().debouncedReflowMenuItems())}),s.setting.notifications.bind("add",e=function(){s.elements.url.element.toggleClass("invalid",s.setting.notifications.has("invalid_url"))}),s.setting.notifications.bind("removed",e)},_setupRemoveUI:function(){var r=this;r.container.find(".item-delete").on("click",function(){var e,t,n,i=!0,a=0,o=r.params.original_item_id,s=r.getMenuControl().$sectionContent.find(".menu-item");m("body").hasClass("adding-menu-items")||(i=!1),n=r.container.nextAll(".customize-control-nav_menu_item:visible").first(),t=r.container.prevAll(".customize-control-nav_menu_item:visible").first(),e=(n.length?n.find(!1===i?".item-edit":".item-delete"):t.length?t.find(!1===i?".item-edit":".item-delete"):r.container.nextAll(".customize-control-nav_menu").find(".add-new-menu-item")).first(),_.each(s,function(e){m(e).is(":visible")&&(e=e.getAttribute("id").match(/^customize-control-nav_menu_item-(-?\d+)$/,""))&&(e=parseInt(e[1],10),e=c.control("nav_menu_item["+String(e)+"]"))&&o==e.params.original_item_id&&a++}),a<=1&&((n=m("#menu-item-tpl-"+r.params.original_item_id)).removeClass("selected"),n.find(".menu-item-handle").removeClass("item-added")),r.container.slideUp(function(){r.setting.set(!1),l.a11y.speak(c.Menus.data.l10n.itemDeleted),e.focus()}),r.setting.set(!1)})},_setupLinksUI:function(){this.container.find("a.original-link").on("click",function(e){e.preventDefault(),c.previewer.previewUrl(e.target.toString())})},_setupTitleUI:function(){var i;this.container.find(".edit-menu-item-title").on("blur",function(){m(this).val(m(this).val().trim())}),i=this.container.find(".menu-item-title"),this.setting.bind(function(e){var t,n;e&&(e.title=e.title||"",n=(t=e.title.trim())||e.original_title||c.Menus.data.l10n.untitled,e._invalid&&(n=c.Menus.data.l10n.invalidTitleTpl.replace("%s",n)),t||e.original_title?i.text(n).removeClass("no-title"):i.text(n).addClass("no-title"))})},getDepth:function(){var e=this,t=e.setting(),n=0;if(!t)return 0;for(;t&&t.menu_item_parent&&(n+=1,e=c.control("nav_menu_item["+t.menu_item_parent+"]"));)t=e.setting();return n},renderContent:function(){var e,t=this,n=t.setting();t.params.title=n.title||"",t.params.depth=t.getDepth(),t.container.data("item-depth",t.params.depth),e=["menu-item","menu-item-depth-"+String(t.params.depth),"menu-item-"+n.object,"menu-item-edit-inactive"],n._invalid?(e.push("menu-item-invalid"),t.params.title=c.Menus.data.l10n.invalidTitleTpl.replace("%s",t.params.title)):"draft"===n.status&&(e.push("pending"),t.params.title=c.Menus.data.pendingTitleTpl.replace("%s",t.params.title)),t.params.el_classes=e.join(" "),t.params.item_type_label=n.type_label,t.params.item_type=n.type,t.params.url=n.url,t.params.target=n.target,t.params.attr_title=n.attr_title,t.params.classes=_.isArray(n.classes)?n.classes.join(" "):n.classes,t.params.xfn=n.xfn,t.params.description=n.description,t.params.parent=n.menu_item_parent,t.params.original_title=n.original_title||"",t.container.addClass(t.params.el_classes),c.Control.prototype.renderContent.call(t)},getMenuControl:function(){var e=this.setting();return e&&e.nav_menu_term_id?c.control("nav_menu["+e.nav_menu_term_id+"]"):null},expandControlSection:function(){var e=this.container.closest(".accordion-section");e.hasClass("open")||e.find(".accordion-section-title:first").trigger("click")},_toggleExpanded:c.Section.prototype._toggleExpanded,expand:c.Section.prototype.expand,expandForm:function(e){this.expand(e)},collapse:c.Section.prototype.collapse,collapseForm:function(e){this.collapse(e)},toggleForm:function(e,t){(e=void 0===e?!this.expanded():e)?this.expand(t):this.collapse(t)},onChangeExpanded:function(e,t){var n,i=this,a=this.container,o=a.find(".menu-item-settings:first");void 0===e&&(e=!o.is(":visible")),o.is(":visible")===e?t&&t.completeCallback&&t.completeCallback():e?(c.control.each(function(e){i.params.type===e.params.type&&i!==e&&e.collapseForm()}),n=function(){a.removeClass("menu-item-edit-inactive").addClass("menu-item-edit-active"),i.container.trigger("expanded"),t&&t.completeCallback&&t.completeCallback()},a.find(".item-edit").attr("aria-expanded","true"),o.slideDown("fast",n),i.container.trigger("expand")):(n=function(){a.addClass("menu-item-edit-inactive").removeClass("menu-item-edit-active"),i.container.trigger("collapsed"),t&&t.completeCallback&&t.completeCallback()},i.container.trigger("collapse"),a.find(".item-edit").attr("aria-expanded","false"),o.slideUp("fast",n))},focus:function(e){var t=this,n=(e=e||{}).completeCallback,i=function(){t.expandControlSection(),e.completeCallback=function(){t.container.find(".menu-item-settings").find("input, select, textarea, button, object, a[href], [tabindex]").filter(":visible").first().focus(),n&&n()},t.expandForm(e)};c.section.has(t.section())?c.section(t.section()).expand({completeCallback:i}):i()},moveUp:function(){this._changePosition(-1),l.a11y.speak(c.Menus.data.l10n.movedUp)},moveDown:function(){this._changePosition(1),l.a11y.speak(c.Menus.data.l10n.movedDown)},moveLeft:function(){this._changeDepth(-1),l.a11y.speak(c.Menus.data.l10n.movedLeft)},moveRight:function(){this._changeDepth(1),l.a11y.speak(c.Menus.data.l10n.movedRight)},_changePosition:function(e){var t,n=this,i=_.clone(n.setting()),a=[];if(1!==e&&-1!==e)throw new Error("Offset changes by 1 are only supported.");if(n.setting()){if(_(n.getMenuControl().getMenuItemControls()).each(function(e){e.setting().menu_item_parent===i.menu_item_parent&&a.push(e.setting)}),a.sort(function(e,t){return e().position-t().position}),-1===(t=_.indexOf(a,n.setting)))throw new Error("Expected setting to be among siblings.");0===t&&e<0||t===a.length-1&&0<e||((t=a[t+e])&&t.set(m.extend(_.clone(t()),{position:i.position})),i.position+=e,n.setting.set(i))}},_changeDepth:function(e){if(1!==e&&-1!==e)throw new Error("Offset changes by 1 are only supported.");var t,n,i=this,a=_.clone(i.setting()),o=[];if(_(i.getMenuControl().getMenuItemControls()).each(function(e){e.setting().menu_item_parent===a.menu_item_parent&&o.push(e)}),o.sort(function(e,t){return e.setting().position-t.setting().position}),-1===(t=_.indexOf(o,i)))throw new Error("Expected control to be among siblings.");-1===e?a.menu_item_parent&&(n=c.control("nav_menu_item["+a.menu_item_parent+"]"),_(o).chain().slice(t).each(function(e,t){e.setting.set(m.extend({},e.setting(),{menu_item_parent:i.params.menu_item_id,position:t}))}),_(i.getMenuControl().getMenuItemControls()).each(function(e){var t;e.setting().menu_item_parent===n.setting().menu_item_parent&&e.setting().position>n.setting().position&&(t=_.clone(e.setting()),e.setting.set(m.extend(t,{position:t.position+1})))}),a.position=n.setting().position+1,a.menu_item_parent=n.setting().menu_item_parent,i.setting.set(a)):1===e&&0!==t&&(a.menu_item_parent=o[t-1].params.menu_item_id,a.position=0,_(i.getMenuControl().getMenuItemControls()).each(function(e){e.setting().menu_item_parent===a.menu_item_parent&&(a.position=Math.max(a.position,e.setting().position))}),a.position+=1,i.setting.set(a))}}),c.Menus.MenuNameControl=c.Control.extend({ready:function(){var e,n=this;n.setting&&(e=n.setting(),n.nameElement=new c.Element(n.container.find(".menu-name-field")),n.nameElement.bind(function(e){var t=n.setting();t&&t.name!==e&&((t=_.clone(t)).name=e,n.setting.set(t))}),e&&n.nameElement.set(e.name),n.setting.bind(function(e){e&&n.nameElement.set(e.name)}))}}),c.Menus.MenuLocationsControl=c.Control.extend({ready:function(){var d=this;d.container.find(".assigned-menu-location").each(function(){function t(e){var t=c("nav_menu["+String(e)+"]");e&&t&&t()?n.find(".theme-location-set").show().find("span").text(u(t().name)):n.find(".theme-location-set").hide()}var n=m(this),e=n.find("input[type=checkbox]"),i=new c.Element(e),a=c("nav_menu_locations["+e.data("location-id")+"]"),o=""===d.params.menu_id,s=o?_.noop:function(e){i.set(e)},r=o?_.noop:function(e){a.set(e?d.params.menu_id:0)};s(a.get()===d.params.menu_id),e.on("change",function(){r(this.checked)}),a.bind(function(e){s(e===d.params.menu_id),t(e)}),t(a.get())})},setSelections:function(i){this.container.find(".menu-location").each(function(e,t){var n=t.dataset.locationId;t.checked=n in i&&i[n]})}}),c.Menus.MenuAutoAddControl=c.Control.extend({ready:function(){var n=this,e=n.setting();n.active.validate=function(){var e=c.section(n.section()),e=!!e&&e.active();return e},n.autoAddElement=new c.Element(n.container.find("input[type=checkbox].auto_add")),n.autoAddElement.bind(function(e){var t=n.setting();t&&t.name!==e&&((t=_.clone(t)).auto_add=e,n.setting.set(t))}),e&&n.autoAddElement.set(e.auto_add),n.setting.bind(function(e){e&&n.autoAddElement.set(e.auto_add)})}}),c.Menus.MenuControl=c.Control.extend({ready:function(){var t,n,i=this,a=c.section(i.section()),o=i.params.menu_id,e=i.setting();if(void 0===this.params.menu_id)throw new Error("params.menu_id was not defined");i.active.validate=function(){var e=!!a&&a.active();return e},i.$controlSection=a.headContainer,i.$sectionContent=i.container.closest(".accordion-section-content"),this._setupModel(),c.section(i.section(),function(e){e.deferred.initSortables.done(function(e){i._setupSortable(e)})}),this._setupAddition(),this._setupTitle(),e&&(t=u(e.name),c.control.each(function(e){e.extended(c.controlConstructor.widget_form)&&"nav_menu"===e.params.widget_id_base&&(e.container.find(".nav-menu-widget-form-controls:first").show(),e.container.find(".nav-menu-widget-no-menus-message:first").hide(),0===(n=e.container.find("select")).find("option[value="+String(o)+"]").length)&&n.append(new Option(t,o))}),(e=m("#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )")).find(".nav-menu-widget-form-controls:first").show(),e.find(".nav-menu-widget-no-menus-message:first").hide(),0===(n=e.find(".widget-inside select:first")).find("option[value="+String(o)+"]").length)&&n.append(new Option(t,o)),_.defer(function(){i.updateInvitationVisibility()})},_setupModel:function(){var n=this,i=n.params.menu_id;n.setting.bind(function(e){var t;!1===e?n._handleDeletion():(t=u(e.name),c.control.each(function(e){e.extended(c.controlConstructor.widget_form)&&"nav_menu"===e.params.widget_id_base&&e.container.find("select").find("option[value="+String(i)+"]").text(t)}))})},_setupSortable:function(e){var a=this;if(!e.is(a.$sectionContent))throw new Error("Unexpected menuList.");e.on("sortstart",function(){a.isSorting=!0}),e.on("sortstop",function(){setTimeout(function(){var e=a.$sectionContent.sortable("toArray"),t=[],n=0,i=10;a.isSorting=!1,a.$sectionContent.scrollLeft(0),_.each(e,function(e){var e=e.match(/^customize-control-nav_menu_item-(-?\d+)$/,"");e&&(e=parseInt(e[1],10),e=c.control("nav_menu_item["+String(e)+"]"))&&t.push(e)}),_.each(t,function(e){var t;!1!==e.setting()&&(t=_.clone(e.setting()),n+=1,i+=1,t.position=n,e.priority(i),t.menu_item_parent=parseInt(e.container.find(".menu-item-data-parent-id").val(),10),t.menu_item_parent||(t.menu_item_parent=0),e.setting.set(t))})})}),a.isReordering=!1,this.container.find(".reorder-toggle").on("click",function(){a.toggleReordering(!a.isReordering)})},_setupAddition:function(){var t=this;this.container.find(".add-new-menu-item").on("click",function(e){t.$sectionContent.hasClass("reordering")||(m("body").hasClass("adding-menu-items")?(m(this).attr("aria-expanded","false"),c.Menus.availableMenuItemsPanel.close(),e.stopPropagation()):(m(this).attr("aria-expanded","true"),c.Menus.availableMenuItemsPanel.open(t)))})},_handleDeletion:function(){var e,n=this.params.menu_id,i=0,t=c.section(this.section()),a=function(){t.container.remove(),c.section.remove(t.id)};t&&t.expanded()?t.collapse({completeCallback:function(){a(),l.a11y.speak(c.Menus.data.l10n.menuDeleted),c.panel("nav_menus").focus()}}):a(),c.each(function(e){/^nav_menu\[/.test(e.id)&&!1!==e()&&(i+=1)}),c.control.each(function(e){var t;e.extended(c.controlConstructor.widget_form)&&"nav_menu"===e.params.widget_id_base&&((t=e.container.find("select")).val()===String(n)&&t.prop("selectedIndex",0).trigger("change"),e.container.find(".nav-menu-widget-form-controls:first").toggle(0!==i),e.container.find(".nav-menu-widget-no-menus-message:first").toggle(0===i),e.container.find("option[value="+String(n)+"]").remove())}),(e=m("#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )")).find(".nav-menu-widget-form-controls:first").toggle(0!==i),e.find(".nav-menu-widget-no-menus-message:first").toggle(0===i),e.find("option[value="+String(n)+"]").remove()},_setupTitle:function(){var d=this;d.setting.bind(function(e){var t,n,i,a,o,s,r;e&&(t=c.section(d.section()),n=d.params.menu_id,i=t.headContainer.find(".accordion-section-title"),a=t.contentContainer.find(".customize-section-title h3"),o=t.headContainer.find(".menu-in-location"),s=a.find(".customize-action"),r=u(e.name),i.text(r),o.length&&o.appendTo(i),a.text(r),s.length&&s.prependTo(a),c.control.each(function(e){/^nav_menu_locations\[/.test(e.id)&&e.container.find("option[value="+n+"]").text(r)}),t.contentContainer.find(".customize-control-checkbox input").each(function(){m(this).prop("checked")&&m(".current-menu-location-name-"+m(this).data("location-id")).text(r)}))})},toggleReordering:function(e){var t=this.container.find(".add-new-menu-item"),n=this.container.find(".reorder-toggle"),i=this.$sectionContent.find(".item-title");(e=Boolean(e))!==this.$sectionContent.hasClass("reordering")&&(this.isReordering=e,this.$sectionContent.toggleClass("reordering",e),this.$sectionContent.sortable(this.isReordering?"disable":"enable"),this.isReordering?(t.attr({tabindex:"-1","aria-hidden":"true"}),n.attr("aria-label",c.Menus.data.l10n.reorderLabelOff),l.a11y.speak(c.Menus.data.l10n.reorderModeOn),i.attr("aria-hidden","false")):(t.removeAttr("tabindex aria-hidden"),n.attr("aria-label",c.Menus.data.l10n.reorderLabelOn),l.a11y.speak(c.Menus.data.l10n.reorderModeOff),i.attr("aria-hidden","true")),e)&&_(this.getMenuItemControls()).each(function(e){e.collapseForm()})},getMenuItemControls:function(){var t=[],n=this.params.menu_id;return c.control.each(function(e){"nav_menu_item"===e.params.type&&e.setting()&&n===e.setting().nav_menu_term_id&&t.push(e)}),t},reflowMenuItems:function(){var e=this.getMenuItemControls(),a=function(n){var t=[],i=n.currentParent;_.each(n.menuItemControls,function(e){i===e.setting().menu_item_parent&&t.push(e)}),t.sort(function(e,t){return e.setting().position-t.setting().position}),_.each(t,function(t){n.currentAbsolutePosition+=1,t.priority.set(n.currentAbsolutePosition),t.container.hasClass("menu-item-depth-"+String(n.currentDepth))||(_.each(t.container.prop("className").match(/menu-item-depth-\d+/g),function(e){t.container.removeClass(e)}),t.container.addClass("menu-item-depth-"+String(n.currentDepth))),t.container.data("item-depth",n.currentDepth),n.currentDepth+=1,n.currentParent=t.params.menu_item_id,a(n),--n.currentDepth,n.currentParent=i}),t.length&&(_(t).each(function(e){e.container.removeClass("move-up-disabled move-down-disabled move-left-disabled move-right-disabled"),0===n.currentDepth?e.container.addClass("move-left-disabled"):10===n.currentDepth&&e.container.addClass("move-right-disabled")}),t[0].container.addClass("move-up-disabled").addClass("move-right-disabled").toggleClass("move-down-disabled",1===t.length),t[t.length-1].container.addClass("move-down-disabled").toggleClass("move-up-disabled",1===t.length))};a({menuItemControls:e,currentParent:0,currentDepth:0,currentAbsolutePosition:0}),this.updateInvitationVisibility(e),this.container.find(".reorder-toggle").toggle(1<e.length)},debouncedReflowMenuItems:_.debounce(function(){this.reflowMenuItems.apply(this,arguments)},0),addItemToMenu:function(e){var t,n,i,a=0,o=10,s=e.id||"";return _.each(this.getMenuItemControls(),function(e){!1!==e.setting()&&(o=Math.max(o,e.priority()),0===e.setting().menu_item_parent)&&(a=Math.max(a,e.setting().position))}),a+=1,o+=1,delete(e=m.extend({},c.Menus.data.defaultSettingValues.nav_menu_item,e,{nav_menu_term_id:this.params.menu_id,original_title:e.title,position:a})).id,i=c.Menus.generatePlaceholderAutoIncrementId(),t="nav_menu_item["+String(i)+"]",n={type:"nav_menu_item",transport:c.Menus.data.settingTransport,previewer:c.previewer},(n=c.create(t,t,{},n)).set(e),e=new c.controlConstructor.nav_menu_item(t,{type:"nav_menu_item",section:this.id,priority:o,settings:{default:t},menu_item_id:i,original_item_id:s}),c.control.add(e),n.preview(),this.debouncedReflowMenuItems(),l.a11y.speak(c.Menus.data.l10n.itemAdded),e},updateInvitationVisibility:function(e){e=e||this.getMenuItemControls();this.container.find(".new-menu-item-invitation").toggle(0===e.length)}}),m.extend(c.controlConstructor,{nav_menu_location:c.Menus.MenuLocationControl,nav_menu_item:c.Menus.MenuItemControl,nav_menu:c.Menus.MenuControl,nav_menu_name:c.Menus.MenuNameControl,nav_menu_locations:c.Menus.MenuLocationsControl,nav_menu_auto_add:c.Menus.MenuAutoAddControl}),m.extend(c.panelConstructor,{nav_menus:c.Menus.MenusPanel}),m.extend(c.sectionConstructor,{nav_menu:c.Menus.MenuSection,new_menu:c.Menus.NewMenuSection}),c.bind("ready",function(){c.Menus.availableMenuItemsPanel=new c.Menus.AvailableMenuItemsPanelView({collection:c.Menus.availableMenuItems}),c.bind("saved",function(e){(e.nav_menu_updates||e.nav_menu_item_updates)&&c.Menus.applySavedData(e)}),c.state("changesetStatus").bind(function(e){"publish"===e&&(c("nav_menus_created_posts")._value=[])}),c.previewer.bind("focus-nav-menu-item-control",c.Menus.focusMenuItemControl)}),c.Menus.applySavedData=function(e){var u={},r={};_(e.nav_menu_updates).each(function(n){var e,t,i,a,o,s,r,d;if("inserted"===n.status){if(!n.previous_term_id)throw new Error("Expected previous_term_id");if(!n.term_id)throw new Error("Expected term_id");if(e="nav_menu["+String(n.previous_term_id)+"]",!c.has(e))throw new Error("Expected setting to exist: "+e);if(i=c(e),!c.section.has(e))throw new Error("Expected control to exist: "+e);if(o=c.section(e),!(s=i.get()))throw new Error("Did not expect setting to be empty (deleted).");s=m.extend(_.clone(s),n.saved_value),u[n.previous_term_id]=n.term_id,a="nav_menu["+String(n.term_id)+"]",t=c.create(a,a,s,{type:"nav_menu",transport:c.Menus.data.settingTransport,previewer:c.previewer}),(d=o.expanded())&&o.collapse(),a=new c.Menus.MenuSection(a,{panel:"nav_menus",title:s.name,customizeAction:c.Menus.data.l10n.customizingMenus,type:"nav_menu",priority:o.priority.get(),menu_id:n.term_id}),c.section.add(a),c.control.each(function(e){var t;e.extended(c.controlConstructor.widget_form)&&"nav_menu"===e.params.widget_id_base&&(t=(e=e.container.find("select")).find("option[value="+String(n.previous_term_id)+"]"),e.find("option[value="+String(n.term_id)+"]").prop("selected",t.prop("selected")),t.remove())}),i.callbacks.disable(),i.set(!1),i.preview(),t.preview(),i._dirty=!1,o.container.remove(),c.section.remove(e),r=0,c.each(function(e){/^nav_menu\[/.test(e.id)&&!1!==e()&&(r+=1)}),(s=m("#available-widgets-list .widget-tpl:has( input.id_base[ value=nav_menu ] )")).find(".nav-menu-widget-form-controls:first").toggle(0!==r),s.find(".nav-menu-widget-no-menus-message:first").toggle(0===r),s.find("option[value="+String(n.previous_term_id)+"]").remove(),l.customize.control.each(function(e){/^nav_menu_locations\[/.test(e.id)&&e.container.find("option[value="+String(n.previous_term_id)+"]").remove()}),c.each(function(e){var t=c.state("saved").get();/^nav_menu_locations\[/.test(e.id)&&e.get()===n.previous_term_id&&(e.set(n.term_id),e._dirty=!1,c.state("saved").set(t),e.preview())}),d&&a.expand()}else if("updated"===n.status){if(t="nav_menu["+String(n.term_id)+"]",!c.has(t))throw new Error("Expected setting to exist: "+t);i=c(t),_.isEqual(n.saved_value,i.get())||(o=c.state("saved").get(),i.set(n.saved_value),i._dirty=!1,c.state("saved").set(o))}}),_(e.nav_menu_item_updates).each(function(e){e.previous_post_id&&(r[e.previous_post_id]=e.post_id)}),_(e.nav_menu_item_updates).each(function(e){var t,n,i,a,o,s;if("inserted"===e.status){if(!e.previous_post_id)throw new Error("Expected previous_post_id");if(!e.post_id)throw new Error("Expected post_id");if(t="nav_menu_item["+String(e.previous_post_id)+"]",!c.has(t))throw new Error("Expected setting to exist: "+t);if(i=c(t),!c.control.has(t))throw new Error("Expected control to exist: "+t);if(o=c.control(t),!(s=i.get()))throw new Error("Did not expect setting to be empty (deleted).");if((s=_.clone(s)).menu_item_parent<0){if(!r[s.menu_item_parent])throw new Error("inserted ID for menu_item_parent not available");s.menu_item_parent=r[s.menu_item_parent]}u[s.nav_menu_term_id]&&(s.nav_menu_term_id=u[s.nav_menu_term_id]),n="nav_menu_item["+String(e.post_id)+"]",a=c.create(n,n,s,{type:"nav_menu_item",transport:c.Menus.data.settingTransport,previewer:c.previewer}),s=new c.controlConstructor.nav_menu_item(n,{type:"nav_menu_item",menu_id:e.post_id,section:"nav_menu["+String(s.nav_menu_term_id)+"]",priority:o.priority.get(),settings:{default:n},menu_item_id:e.post_id}),o.container.remove(),c.control.remove(t),c.control.add(s),i.callbacks.disable(),i.set(!1),i.preview(),a.preview(),i._dirty=!1,s.container.toggleClass("menu-item-edit-inactive",o.container.hasClass("menu-item-edit-inactive"))}}),_.each(e.widget_nav_menu_updates,function(e,t){t=c(t);t&&(t._value=e,t.preview())})},c.Menus.focusMenuItemControl=function(e){e=c.Menus.getMenuItemControl(e);e&&e.focus()},c.Menus.getMenuControl=function(e){return c.control("nav_menu["+e+"]")},c.Menus.getMenuItemControl=function(e){return c.control("nav_menu_item["+e+"]")}}(wp.customize,wp,jQuery);
\ No newline at end of file diff --git a/wp-admin/js/customize-widgets.js b/wp-admin/js/customize-widgets.js new file mode 100644 index 0000000..2ba8aee --- /dev/null +++ b/wp-admin/js/customize-widgets.js @@ -0,0 +1,2372 @@ +/** + * @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 accessiblity 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 = $( '<div>' + newForm + '</div>' ).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 = $( '<div>' + r.data.form + '</div>' ); + $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( '<p class="widget-error"><strong>' + message + '</strong></p>' ); + } + } + } ); + + 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 <https://core.trac.wordpress.org/ticket/38794>. + noticeContainer = $( '<div></div>', { + 'class': 'no-widget-areas-rendered-notice' + }); + 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( $( '<p></p>', { + text: someRenderedMessage + } ) ); + } + + noticeContainer.append( $( '<p></p>', { + 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 = $( '<li/>' ) + .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 ); diff --git a/wp-admin/js/customize-widgets.min.js b/wp-admin/js/customize-widgets.min.js new file mode 100644 index 0000000..83179a9 --- /dev/null +++ b/wp-admin/js/customize-widgets.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(u,h){var p,f;function c(e){var t={number:null,id_base:null},i=e.match(/^(.+)-(\d+)$/);return i?(t.id_base=i[1],t.number=parseInt(i[2],10)):t.id_base=e,t}u&&u.customize&&((p=u.customize).Widgets=p.Widgets||{},p.Widgets.savedWidgetIds={},p.Widgets.data=_wpCustomizeWidgetsSettings||{},f=p.Widgets.data.l10n,p.Widgets.WidgetModel=Backbone.Model.extend({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:!0}),p.Widgets.WidgetCollection=Backbone.Collection.extend({model:p.Widgets.WidgetModel,doSearch:function(e){this.terms!==e&&(this.terms=e,0<this.terms.length&&this.search(this.terms),""===this.terms)&&this.each(function(e){e.set("search_matched",!0)})},search:function(e){var t,i;e=(e=e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")).replace(/ /g,")(?=.*"),t=new RegExp("^(?=.*"+e+").+","i"),this.each(function(e){i=[e.get("name"),e.get("description")].join(" "),e.set("search_matched",t.test(i))})}}),p.Widgets.availableWidgets=new p.Widgets.WidgetCollection(p.Widgets.data.availableWidgets),p.Widgets.SidebarModel=Backbone.Model.extend({after_title:null,after_widget:null,before_title:null,before_widget:null,class:null,description:null,id:null,name:null,is_rendered:!1}),p.Widgets.SidebarCollection=Backbone.Collection.extend({model:p.Widgets.SidebarModel}),p.Widgets.registeredSidebars=new p.Widgets.SidebarCollection(p.Widgets.data.registeredSidebars),p.Widgets.AvailableWidgetsPanelView=u.Backbone.View.extend({el:"#available-widgets",events:{"input #widgets-search":"search","focus .widget-tpl":"focus","click .widget-tpl":"_submit","keypress .widget-tpl":"_submit",keydown:"keyboardAccessible"},selected:null,currentSidebarControl:null,$search:null,$clearResults:null,searchMatchesCount:null,initialize:function(){var t=this;this.$search=h("#widgets-search"),this.$clearResults=this.$el.find(".clear-results"),_.bindAll(this,"close"),this.listenTo(this.collection,"change",this.updateList),this.updateList(),this.searchMatchesCount=this.collection.length,h("#customize-controls, #available-widgets .customize-section-title").on("click keydown",function(e){e=h(e.target).is(".add-new-widget, .add-new-widget *");h("body").hasClass("adding-widget")&&!e&&t.close()}),this.$clearResults.on("click",function(){t.$search.val("").trigger("focus").trigger("input")}),p.previewer.bind("url",this.close)},search:_.debounce(function(e){var t;this.collection.doSearch(e.target.value),this.updateSearchMatchesCount(),this.announceSearchMatches(),this.selected&&!this.selected.is(":visible")&&(this.selected.removeClass("selected"),this.selected=null),this.selected&&!e.target.value&&(this.selected.removeClass("selected"),this.selected=null),!this.selected&&e.target.value&&(t=this.$el.find("> .widget-tpl:visible:first")).length&&this.select(t),""!==e.target.value?this.$clearResults.addClass("is-visible"):""===e.target.value&&this.$clearResults.removeClass("is-visible"),this.searchMatchesCount?this.$el.removeClass("no-widgets-found"):this.$el.addClass("no-widgets-found")},500),updateSearchMatchesCount:function(){this.searchMatchesCount=this.collection.where({search_matched:!0}).length},announceSearchMatches:function(){var e=f.widgetsFound.replace("%d",this.searchMatchesCount);this.searchMatchesCount||(e=f.noWidgetsFound),u.a11y.speak(e)},updateList:function(){this.collection.each(function(e){var t=h("#widget-tpl-"+e.id);t.toggle(e.get("search_matched")&&!e.get("is_disabled")),e.get("is_disabled")&&t.is(this.selected)&&(this.selected=null)})},select:function(e){this.selected=h(e),this.selected.siblings(".widget-tpl").removeClass("selected"),this.selected.addClass("selected")},focus:function(e){this.select(h(e.currentTarget))},_submit:function(e){"keypress"===e.type&&13!==e.which&&32!==e.which||this.submit(h(e.currentTarget))},submit:function(e){(e=e||this.selected)&&this.currentSidebarControl&&(this.select(e),e=h(this.selected).data("widget-id"),e=this.collection.findWhere({id:e}))&&((e=this.currentSidebarControl.addWidget(e.get("id_base")))&&e.focus(),this.close())},open:function(e){this.currentSidebarControl=e,_(this.currentSidebarControl.getWidgetFormControls()).each(function(e){e.params.is_wide&&e.collapseForm()}),p.section.has("publish_settings")&&p.section("publish_settings").collapse(),h("body").addClass("adding-widget"),this.$el.find(".selected").removeClass("selected"),this.collection.doSearch(""),p.settings.browser.mobile||this.$search.trigger("focus")},close:function(e){(e=e||{}).returnFocus&&this.currentSidebarControl&&this.currentSidebarControl.container.find(".add-new-widget").focus(),this.currentSidebarControl=null,this.selected=null,h("body").removeClass("adding-widget"),this.$search.val("").trigger("input")},keyboardAccessible:function(e){var t=13===e.which,i=27===e.which,n=40===e.which,s=38===e.which,d=9===e.which,a=e.shiftKey,o=null,r=this.$el.find("> .widget-tpl:visible:first"),l=this.$el.find("> .widget-tpl:visible:last"),c=h(e.target).is(this.$search),g=h(e.target).is(".widget-tpl:visible:last");n||s?(n?c?o=r:this.selected&&0!==this.selected.nextAll(".widget-tpl:visible").length&&(o=this.selected.nextAll(".widget-tpl:visible:first")):s&&(c?o=l:this.selected&&0!==this.selected.prevAll(".widget-tpl:visible").length&&(o=this.selected.prevAll(".widget-tpl:visible:first"))),this.select(o),(o||this.$search).trigger("focus")):t&&!this.$search.val()||(t?this.submit():i&&this.close({returnFocus:!0}),this.currentSidebarControl&&d&&(a&&c||!a&&g)&&(this.currentSidebarControl.container.find(".add-new-widget").focus(),e.preventDefault()))}}),p.Widgets.formSyncHandlers={rss:function(e,t,i){var n=t.find(".widget-error:first"),i=h("<div>"+i+"</div>").find(".widget-error:first");n.length&&i.length?n.replaceWith(i):n.length?n.remove():i.length&&t.find(".widget-content:first").prepend(i)}},p.Widgets.WidgetControl=p.Control.extend({defaultExpandedArguments:{duration:"fast",completeCallback:h.noop},initialize:function(e,t){var i=this;i.widgetControlEmbedded=!1,i.widgetContentEmbedded=!1,i.expanded=new p.Value(!1),i.expandedArgumentsQueue=[],i.expanded.bind(function(e){var t=i.expandedArgumentsQueue.shift(),t=h.extend({},i.defaultExpandedArguments,t);i.onChangeExpanded(e,t)}),i.altNotice=!0,p.Control.prototype.initialize.call(i,e,t)},ready:function(){var n=this;n.section()?p.section(n.section(),function(t){function i(e){e&&(n.embedWidgetControl(),t.expanded.unbind(i))}t.expanded()?i(!0):t.expanded.bind(i)}):n.embedWidgetControl()},embedWidgetControl:function(){var e,t=this;t.widgetControlEmbedded||(t.widgetControlEmbedded=!0,e=h(t.params.widget_control),t.container.append(e),t._setupModel(),t._setupWideWidget(),t._setupControlToggle(),t._setupWidgetTitle(),t._setupReorderUI(),t._setupHighlightEffects(),t._setupUpdateUI(),t._setupRemoveUI())},embedWidgetContent:function(){var e,t=this;t.embedWidgetControl(),t.widgetContentEmbedded||(t.widgetContentEmbedded=!0,t.notifications.container=t.getNotificationsContainerElement(),t.notifications.render(),e=h(t.params.widget_content),t.container.find(".widget-content:first").append(e),h(document).trigger("widget-added",[t.container.find(".widget:first")]))},_setupModel:function(){var i=this,e=function(){p.Widgets.savedWidgetIds[i.params.widget_id]=!0};p.bind("ready",e),p.bind("saved",e),this._updateCount=0,this.isWidgetUpdating=!1,this.liveUpdateMode=!0,this.setting.bind(function(e,t){_(t).isEqual(e)||i.isWidgetUpdating||i.updateWidget({instance:e})})},_setupWideWidget:function(){var n,s,e,t,i,d=this;!this.params.is_wide||h(window).width()<=640||(n=this.container.find(".widget-inside"),s=n.find("> .form"),e=h(".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}),i=function(){var e=d.container.offset().top,t=h(window).height(),i=s.outerHeight();n.css("max-height",t),e=Math.max(0,Math.min(Math.max(e,0),t-i)),n.css("top",e)},t=h("#customize-theme-controls"),this.container.on("expand",function(){i(),e.on("scroll",i),h(window).on("resize",i),t.on("expanded collapsed",i)}),this.container.on("collapsed",function(){e.off("scroll",i),h(window).off("resize",i),t.off("expanded collapsed",i)}),p.each(function(e){0===e.id.indexOf("sidebars_widgets[")&&e.bind(function(){d.container.hasClass("expanded")&&i()})}))},_setupControlToggle:function(){var t=this;this.container.find(".widget-top").on("click",function(e){e.preventDefault(),t.getSidebarWidgetsControl().isReordering||t.expanded(!t.expanded())}),this.container.find(".widget-control-close").on("click",function(){t.collapse(),t.container.find(".widget-top .widget-action:first").focus()})},_setupWidgetTitle:function(){var i=this,e=function(){var e=i.setting().title,t=i.container.find(".in-widget-title");e?t.text(": "+e):t.text("")};this.setting.bind(e),e()},_setupReorderUI:function(){var t,e,d=this,s=function(e){e.siblings(".selected").removeClass("selected"),e.addClass("selected");e=e.data("id")===d.params.sidebar_id;d.container.find(".move-widget-btn").prop("disabled",e)};this.container.find(".widget-title-action").after(h(p.Widgets.data.tpl.widgetReorderNav)),e=_.template(p.Widgets.data.tpl.moveWidgetArea),t=h(e({sidebars:_(p.Widgets.registeredSidebars.toArray()).pluck("attributes")})),this.container.find(".widget-top").after(t),(e=function(){var e=t.find("li"),i=0,n=e.filter(function(){return h(this).data("id")===d.params.sidebar_id});e.each(function(){var e=h(this),t=e.data("id"),t=p.Widgets.registeredSidebars.get(t).get("is_rendered");e.toggle(t),t&&(i+=1),e.hasClass("selected")&&!t&&s(n)}),1<i?d.container.find(".move-widget").show():d.container.find(".move-widget").hide()})(),p.Widgets.registeredSidebars.on("change:is_rendered",e),this.container.find(".widget-reorder-nav").find(".move-widget, .move-widget-down, .move-widget-up").each(function(){h(this).prepend(d.container.find(".widget-title").text()+": ")}).on("click keypress",function(e){var t,i;"keypress"===e.type&&13!==e.which&&32!==e.which||(h(this).trigger("focus"),h(this).is(".move-widget")?d.toggleWidgetMoveArea():(e=h(this).is(".move-widget-down"),t=h(this).is(".move-widget-up"),i=d.getWidgetSidebarPosition(),t&&0===i||e&&i===d.getSidebarWidgetsControl().setting().length-1||(t?(d.moveUp(),u.a11y.speak(f.widgetMovedUp)):(d.moveDown(),u.a11y.speak(f.widgetMovedDown)),h(this).trigger("focus"))))}),this.container.find(".widget-area-select").on("click keypress","li",function(e){"keypress"===e.type&&13!==e.which&&32!==e.which||(e.preventDefault(),s(h(this)))}),this.container.find(".move-widget-btn").click(function(){d.getSidebarWidgetsControl().toggleReordering(!1);var e=d.params.sidebar_id,t=d.container.find(".widget-area-select li.selected").data("id"),e=p("sidebars_widgets["+e+"]"),t=p("sidebars_widgets["+t+"]"),i=Array.prototype.slice.call(e()),n=Array.prototype.slice.call(t()),s=d.getWidgetSidebarPosition();i.splice(s,1),n.push(d.params.widget_id),e(i),t(n),d.focus()})},_setupHighlightEffects:function(){var e=this;this.container.on("mouseenter click",function(){e.setting.previewer.send("highlight-widget",e.params.widget_id)}),this.setting.bind(function(){e.setting.previewer.send("highlight-widget",e.params.widget_id)})},_setupUpdateUI:function(){var t,i,n=this,s=this.container.find(".widget:first"),e=s.find(".widget-content:first"),d=this.container.find(".widget-control-save");d.val(f.saveBtnLabel),d.attr("title",f.saveBtnTooltip),d.removeClass("button-primary"),d.on("click",function(e){e.preventDefault(),n.updateWidget({disable_form:!0})}),t=_.debounce(function(){n.updateWidget()},250),e.on("keydown","input",function(e){13===e.which&&(e.preventDefault(),n.updateWidget({ignoreActiveElement:!0}))}),e.on("change input propertychange",":input",function(e){n.liveUpdateMode&&("change"===e.type||this.checkValidity&&this.checkValidity())&&t()}),this.setting.previewer.channel.bind("synced",function(){n.container.removeClass("previewer-loading")}),p.previewer.bind("widget-updated",function(e){e===n.params.widget_id&&n.container.removeClass("previewer-loading")}),(i=p.Widgets.formSyncHandlers[this.params.widget_id_base])&&h(document).on("widget-synced",function(e,t){s.is(t)&&i.apply(document,arguments)})},onChangeActive:function(e,t){this.container.toggleClass("widget-rendered",e),t.completeCallback&&t.completeCallback()},_setupRemoveUI:function(){var e,s=this,t=this.container.find(".widget-control-remove");t.on("click",function(){var n=s.container.next().is(".customize-control-widget_form")?s.container.next().find(".widget-action:first"):s.container.prev().is(".customize-control-widget_form")?s.container.prev().find(".widget-action:first"):s.container.next(".customize-control-sidebar_widgets").find(".add-new-widget:first");s.container.slideUp(function(){var e,t,i=p.Widgets.getSidebarWidgetControlContainingWidget(s.params.widget_id);i&&(e=i.setting().slice(),-1!==(t=_.indexOf(e,s.params.widget_id)))&&(e.splice(t,1),i.setting(e),n.focus())})}),e=function(){t.text(f.removeBtnLabel),t.attr("title",f.removeBtnTooltip)},this.params.is_new?p.bind("saved",e):e()},_getInputs:function(e){return h(e).find(":input[name]")},_getInputsSignature:function(e){return _(e).map(function(e){e=h(e),e=e.is(":checkbox, :radio")?[e.attr("id"),e.attr("name"),e.prop("value")]:[e.attr("id"),e.attr("name")];return e.join(",")}).join(";")},_getInputState:function(e){return(e=h(e)).is(":radio, :checkbox")?e.prop("checked"):e.is("select[multiple]")?e.find("option:selected").map(function(){return h(this).val()}).get():e.val()},_setInputState:function(e,t){(e=h(e)).is(":radio, :checkbox")?e.prop("checked",t):e.is("select[multiple]")?(t=Array.isArray(t)?_.map(t,function(e){return String(e)}):[],e.find("option").each(function(){h(this).prop("selected",-1!==_.indexOf(t,String(this.value)))})):e.val(t)},getSidebarWidgetsControl:function(){var e="sidebars_widgets["+this.params.sidebar_id+"]",e=p.control(e);if(e)return e},updateWidget:function(s){var d,a,o,r,e,l,t,i,c,g=this;g.embedWidgetContent(),i=(s=h.extend({instance:null,complete:null,ignoreActiveElement:!1},s)).instance,d=s.complete,this._updateCount+=1,r=this._updateCount,a=this.container.find(".widget:first"),(o=a.find(".widget-content:first")).find(".widget-error").remove(),this.container.addClass("widget-form-loading"),this.container.addClass("previewer-loading"),(t=p.state("processing"))(t()+1),this.liveUpdateMode||this.container.addClass("widget-form-disabled"),(e={action:"update-widget",wp_customize:"on"}).nonce=p.settings.nonce["update-widget"],e.customize_theme=p.settings.theme.stylesheet,e.customized=u.customize.previewer.query().customized,e=h.param(e),(l=this._getInputs(o)).each(function(){h(this).data("state"+r,g._getInputState(this))}),e=(e+=i?"&"+h.param({sanitized_widget_setting:JSON.stringify(i)}):"&"+l.serialize())+"&"+o.find("~ :input").serialize(),this._previousUpdateRequest&&this._previousUpdateRequest.abort(),i=h.post(u.ajax.settings.url,e),(this._previousUpdateRequest=i).done(function(e){var n,t,i=!1;"0"===e?(p.previewer.preview.iframe.hide(),p.previewer.login().done(function(){g.updateWidget(s),p.previewer.preview.iframe.show()})):"-1"===e?p.previewer.cheatin():e.success?(t=h("<div>"+e.data.form+"</div>"),n=g._getInputs(t),(t=g._getInputsSignature(l)===g._getInputsSignature(n))&&!g.liveUpdateMode&&(g.liveUpdateMode=!0,g.container.removeClass("widget-form-disabled"),g.container.find('input[name="savewidget"]').hide()),t&&g.liveUpdateMode?(l.each(function(e){var t=h(this),e=h(n[e]),i=t.data("state"+r),e=g._getInputState(e);t.data("sanitized",e),_.isEqual(i,e)||!s.ignoreActiveElement&&t.is(document.activeElement)||g._setInputState(t,e)}),h(document).trigger("widget-synced",[a,e.data.form])):g.liveUpdateMode?(g.liveUpdateMode=!1,g.container.find('input[name="savewidget"]').show(),i=!0):(o.html(e.data.form),g.container.removeClass("widget-form-disabled"),h(document).trigger("widget-updated",[a])),(c=!i&&!_(g.setting()).isEqual(e.data.instance))?(g.isWidgetUpdating=!0,g.setting(e.data.instance),g.isWidgetUpdating=!1):g.container.removeClass("previewer-loading"),d&&d.call(g,null,{noChange:!c,ajaxFinished:!0})):(t=f.error,e.data&&e.data.message&&(t=e.data.message),d?d.call(g,t):o.prepend('<p class="widget-error"><strong>'+t+"</strong></p>"))}),i.fail(function(e,t){d&&d.call(g,t)}),i.always(function(){g.container.removeClass("widget-form-loading"),l.each(function(){h(this).removeData("state"+r)}),t(t()-1)})},expandControlSection:function(){p.Control.prototype.expand.call(this)},_toggleExpanded:p.Section.prototype._toggleExpanded,expand:p.Section.prototype.expand,expandForm:function(){this.expand()},collapse:p.Section.prototype.collapse,collapseForm:function(){this.collapse()},toggleForm:function(e){void 0===e&&(e=!this.expanded()),this.expanded(e)},onChangeExpanded:function(e,t){var i,n,s,d,a,o=this;o.embedWidgetControl(),e&&o.embedWidgetContent(),t.unchanged?e&&p.Control.prototype.expand.call(o,{completeCallback:t.completeCallback}):(i=this.container.find("div.widget:first"),n=i.find(".widget-inside:first"),e=function(){p.control.each(function(e){o.params.type===e.params.type&&o!==e&&e.collapse()}),s=function(){o.container.removeClass("expanding"),o.container.addClass("expanded"),i.addClass("open"),a.attr("aria-expanded","true"),o.container.trigger("expanded")},t.completeCallback&&(d=s,s=function(){d(),t.completeCallback()}),o.params.is_wide?n.fadeIn(t.duration,s):n.slideDown(t.duration,s),o.container.trigger("expand"),o.container.addClass("expanding")},"false"===(a=this.container.find(".widget-top button.widget-action")).attr("aria-expanded")?p.section.has(o.section())?p.section(o.section()).expand({completeCallback:e}):e():(s=function(){o.container.removeClass("collapsing"),o.container.removeClass("expanded"),i.removeClass("open"),a.attr("aria-expanded","false"),o.container.trigger("collapsed")},t.completeCallback&&(d=s,s=function(){d(),t.completeCallback()}),o.container.trigger("collapse"),o.container.addClass("collapsing"),o.params.is_wide?n.fadeOut(t.duration,s):n.slideUp(t.duration,function(){i.css({width:"",margin:""}),s()})))},getWidgetSidebarPosition:function(){var e=this.getSidebarWidgetsControl().setting(),e=_.indexOf(e,this.params.widget_id);if(-1!==e)return e},moveUp:function(){this._moveWidgetByOne(-1)},moveDown:function(){this._moveWidgetByOne(1)},_moveWidgetByOne:function(e){var t=this.getWidgetSidebarPosition(),i=this.getSidebarWidgetsControl().setting,n=Array.prototype.slice.call(i()),s=n[t+e];n[t+e]=this.params.widget_id,n[t]=s,i(n)},toggleWidgetMoveArea:function(e){var t=this,i=this.container.find(".move-widget-area");(e=void 0===e?!i.hasClass("active"):e)&&(i.find(".selected").removeClass("selected"),i.find("li").filter(function(){return h(this).data("id")===t.params.sidebar_id}).addClass("selected"),this.container.find(".move-widget-btn").prop("disabled",!0)),i.toggleClass("active",e)},highlightSectionAndControl:function(){var e=this.container.is(":hidden")?this.container.closest(".control-section"):this.container;h(".highlighted").removeClass("highlighted"),e.addClass("highlighted"),setTimeout(function(){e.removeClass("highlighted")},500)}}),p.Widgets.WidgetsPanel=p.Panel.extend({ready:function(){var d=this;p.Panel.prototype.ready.call(d),d.deferred.embedded.done(function(){var t,i,n,e=d.container.find(".panel-meta"),s=h("<div></div>",{class:"no-widget-areas-rendered-notice"});e.append(s),i=function(){return _.filter(d.sections(),function(e){return"sidebar"===e.params.type&&e.active()}).length},n=function(){var e=i();return 0===e||e!==p.Widgets.data.registeredSidebars.length},(t=function(){var e,t=i();s.empty(),t!==(e=p.Widgets.data.registeredSidebars.length)&&((e=0!==t?f.someAreasShown[e-t]:f.noAreasShown)&&s.append(h("<p></p>",{text:e})),s.append(h("<p></p>",{text:f.navigatePreview})))})(),s.toggle(n()),p.previewer.deferred.active.done(function(){s.toggle(n())}),p.bind("pane-contents-reflowed",function(){var e="resolved"===p.previewer.deferred.active.state()?"fast":0;t(),n()?s.slideDown(e):s.slideUp(e)})})},isContextuallyActive:function(){return this.active()}}),p.Widgets.SidebarSection=p.Section.extend({ready:function(){var t;p.Section.prototype.ready.call(this),t=p.Widgets.registeredSidebars.get(this.params.sidebarId),this.active.bind(function(e){t.set("is_rendered",e)}),t.set("is_rendered",this.active())}}),p.Widgets.SidebarControl=p.Control.extend({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()},_setupModel:function(){var s=this;this.setting.bind(function(i,e){var t,n,e=_(e).difference(i);i=_(i).filter(function(e){e=c(e);return!!p.Widgets.availableWidgets.findWhere({id_base:e.id_base})}),(t=_(i).map(function(e){return p.Widgets.getWidgetFormControlForWidget(e)||s.addWidget(e)})).sort(function(e,t){return _.indexOf(i,e.params.widget_id)-_.indexOf(i,t.params.widget_id)}),n=0,_(t).each(function(e){e.priority(n),e.section(s.section()),n+=1}),s.priority(n),s._applyCardinalOrderClassNames(),_(t).each(function(e){e.params.sidebar_id=s.params.sidebar_id}),_(e).each(function(n){setTimeout(function(){var e,t,i=!1;p.each(function(e){e.id!==s.setting.id&&0===e.id.indexOf("sidebars_widgets[")&&"sidebars_widgets[wp_inactive_widgets]"!==e.id&&(e=e(),-1!==_.indexOf(e,n))&&(i=!0)}),i||(t=(e=p.Widgets.getWidgetFormControlForWidget(n))&&h.contains(document,e.container[0])&&!h.contains(s.$sectionContent[0],e.container[0]),e&&!t&&(p.control.remove(e.id),e.container.remove()),p.Widgets.savedWidgetIds[n]&&((t=p.value("sidebars_widgets[wp_inactive_widgets]")().slice()).push(n),p.value("sidebars_widgets[wp_inactive_widgets]")(_(t).unique())),e=c(n).id_base,(t=p.Widgets.availableWidgets.findWhere({id_base:e}))&&!t.get("is_multi")&&t.set("is_disabled",!1))})})})},_setupSortable:function(){var t=this;this.isReordering=!1,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 e=t.$sectionContent.sortable("toArray"),e=h.map(e,function(e){return h("#"+e).find(":input[name=widget-id]").val()});t.setting(e)}}),this.$controlSection.find(".accordion-section-title").droppable({accept:".customize-control-widget_form",over:function(){p.section(t.section.get()).expand({allowMultiple:!0,completeCallback:function(){p.section.each(function(e){e.container.find(".customize-control-sidebar_widgets").length&&e.container.find(".accordion-section-content:first").sortable("refreshPositions")})}})}}),this.container.find(".reorder-toggle").on("click",function(){t.toggleReordering(!t.isReordering)})},_setupAddition:function(){var t=this;this.container.find(".add-new-widget").on("click",function(){var e=h(this);t.$sectionContent.hasClass("reordering")||(h("body").hasClass("adding-widget")?(e.attr("aria-expanded","false"),p.Widgets.availableWidgetsPanel.close()):(e.attr("aria-expanded","true"),p.Widgets.availableWidgetsPanel.open(t)))})},_applyCardinalOrderClassNames:function(){var t=[];_.each(this.setting(),function(e){e=p.Widgets.getWidgetFormControlForWidget(e);e&&t.push(e)}),0===t.length||1===p.Widgets.registeredSidebars.length&&t.length<=1?this.container.find(".reorder-toggle").hide():(this.container.find(".reorder-toggle").show(),h(t).each(function(){h(this.container).removeClass("first-widget").removeClass("last-widget").find(".move-widget-down, .move-widget-up").prop("tabIndex",0)}),_.first(t).container.addClass("first-widget").find(".move-widget-up").prop("tabIndex",-1),_.last(t).container.addClass("last-widget").find(".move-widget-down").prop("tabIndex",-1))},toggleReordering:function(e){var t=this.$sectionContent.find(".add-new-widget"),i=this.container.find(".reorder-toggle"),n=this.$sectionContent.find(".widget-title");(e=Boolean(e))!==this.$sectionContent.hasClass("reordering")&&(this.isReordering=e,this.$sectionContent.toggleClass("reordering",e),e?(_(this.getWidgetFormControls()).each(function(e){e.collapse()}),t.attr({tabindex:"-1","aria-hidden":"true"}),i.attr("aria-label",f.reorderLabelOff),u.a11y.speak(f.reorderModeOn),n.attr("aria-hidden","true")):(t.removeAttr("tabindex aria-hidden"),i.attr("aria-label",f.reorderLabelOn),u.a11y.speak(f.reorderModeOff),n.attr("aria-hidden","false")))},getWidgetFormControls:function(){var t=[];return _(this.setting()).each(function(e){e=function(e){var t,e=c(e);t="widget_"+e.id_base,e.number&&(t+="["+e.number+"]");return t}(e),e=p.control(e);e&&t.push(e)}),t},addWidget:function(n){var e,t,i,s,d,a=this,o="widget_form",r=c(n),l=r.number,r=r.id_base,r=p.Widgets.availableWidgets.findWhere({id_base:r});return!(!r||l&&!r.get("is_multi"))&&(r.get("is_multi")&&!l&&(r.set("multi_number",r.get("multi_number")+1),l=r.get("multi_number")),e=h("#widget-tpl-"+r.get("id")).html().trim(),r.get("is_multi")?e=e.replace(/<[^<>]+>/g,function(e){return e.replace(/__i__|%i%/g,l)}):r.set("is_disabled",!0),e=h(e),(e=h("<li/>").addClass("customize-control").addClass("customize-control-"+o).append(e)).find("> .widget-icon").remove(),r.get("is_multi")&&(e.find('input[name="widget_number"]').val(l),e.find('input[name="multi_number"]').val(l)),n=e.find('[name="widget-id"]').val(),e.hide(),t="widget_"+r.get("id_base"),r.get("is_multi")&&(t+="["+l+"]"),e.attr("id","customize-control-"+t.replace(/\]/g,"").replace(/\[/g,"-")),(i=p.has(t))||(d={transport:p.Widgets.data.selectiveRefreshableWidgets[r.get("id_base")]?"postMessage":"refresh",previewer:this.setting.previewer},p.create(t,t,"",d).set({})),d=p.controlConstructor[o],s=new d(t,{settings:{default:t},content:e,sidebar_id:a.params.sidebar_id,widget_id:n,widget_id_base:r.get("id_base"),type:o,is_new:!i,width:r.get("width"),height:r.get("height"),is_wide:r.get("is_wide")}),p.control.add(s),p.each(function(e){var t,i;e.id!==a.setting.id&&0===e.id.indexOf("sidebars_widgets[")&&(t=e().slice(),-1!==(i=_.indexOf(t,n)))&&(t.splice(i),e(t))}),d=this.setting().slice(),-1===_.indexOf(d,n)&&(d.push(n),this.setting(d)),e.slideDown(function(){i&&s.updateWidget({instance:s.setting()})}),s)}}),h.extend(p.panelConstructor,{widgets:p.Widgets.WidgetsPanel}),h.extend(p.sectionConstructor,{sidebar:p.Widgets.SidebarSection}),h.extend(p.controlConstructor,{widget_form:p.Widgets.WidgetControl,sidebar_widgets:p.Widgets.SidebarControl}),p.bind("ready",function(){p.Widgets.availableWidgetsPanel=new p.Widgets.AvailableWidgetsPanelView({collection:p.Widgets.availableWidgets}),p.previewer.bind("highlight-widget-control",p.Widgets.highlightWidgetFormControl),p.previewer.bind("focus-widget-control",p.Widgets.focusWidgetFormControl)}),p.Widgets.highlightWidgetFormControl=function(e){e=p.Widgets.getWidgetFormControlForWidget(e);e&&e.highlightSectionAndControl()},p.Widgets.focusWidgetFormControl=function(e){e=p.Widgets.getWidgetFormControlForWidget(e);e&&e.focus()},p.Widgets.getSidebarWidgetControlContainingWidget=function(t){var i=null;return p.control.each(function(e){"sidebar_widgets"===e.params.type&&-1!==_.indexOf(e.setting(),t)&&(i=e)}),i},p.Widgets.getWidgetFormControlForWidget=function(t){var i=null;return p.control.each(function(e){"widget_form"===e.params.type&&e.params.widget_id===t&&(i=e)}),i},h(document).on("widget-added",function(e,t){var s,d,i,n=c(t.find("> .widget-inside > .form > .widget-id").val());"nav_menu"===n.id_base&&(s=p.control("widget_nav_menu["+String(n.number)+"]"))&&(d=t.find('select[name*="nav_menu"]'),i=t.find(".edit-selected-nav-menu > button"),0!==d.length)&&0!==i.length&&(d.on("change",function(){p.section.has("nav_menu["+d.val()+"]")?i.parent().show():i.parent().hide()}),i.on("click",function(){var i,n,e=p.section("nav_menu["+d.val()+"]");e&&(n=s,(i=e).focus(),i.expanded.bind(function e(t){t||(i.expanded.unbind(e),n.focus())}))}))}))}(window.wp,jQuery);
\ No newline at end of file diff --git a/wp-admin/js/dashboard.js b/wp-admin/js/dashboard.js new file mode 100644 index 0000000..3354790 --- /dev/null +++ b/wp-admin/js/dashboard.js @@ -0,0 +1,839 @@ +/** + * @output wp-admin/js/dashboard.js + */ + +/* global pagenow, ajaxurl, postboxes, wpActiveEditor:true, ajaxWidgets */ +/* global ajaxPopulateWidgets, quickPressLoad, */ +window.wp = window.wp || {}; +window.communityEventsData = window.communityEventsData || {}; + +/** + * Initializes the dashboard widget functionality. + * + * @since 2.7.0 + */ +jQuery( function($) { + var welcomePanel = $( '#welcome-panel' ), + welcomePanelHide = $('#wp_welcome_panel-hide'), + updateWelcomePanel; + + /** + * Saves the visibility of the welcome panel. + * + * @since 3.3.0 + * + * @param {boolean} visible Should it be visible or not. + * + * @return {void} + */ + updateWelcomePanel = function( visible ) { + $.post( ajaxurl, { + action: 'update-welcome-panel', + visible: visible, + welcomepanelnonce: $( '#welcomepanelnonce' ).val() + }); + }; + + // Unhide the welcome panel if the Welcome Option checkbox is checked. + if ( welcomePanel.hasClass('hidden') && welcomePanelHide.prop('checked') ) { + welcomePanel.removeClass('hidden'); + } + + // Hide the welcome panel when the dismiss button or close button is clicked. + $('.welcome-panel-close, .welcome-panel-dismiss a', welcomePanel).on( 'click', function(e) { + e.preventDefault(); + welcomePanel.addClass('hidden'); + updateWelcomePanel( 0 ); + $('#wp_welcome_panel-hide').prop('checked', false); + }); + + // Set welcome panel visibility based on Welcome Option checkbox value. + welcomePanelHide.on( 'click', function() { + welcomePanel.toggleClass('hidden', ! this.checked ); + updateWelcomePanel( this.checked ? 1 : 0 ); + }); + + /** + * These widgets can be populated via ajax. + * + * @since 2.7.0 + * + * @type {string[]} + * + * @global + */ + window.ajaxWidgets = ['dashboard_primary']; + + /** + * Triggers widget updates via Ajax. + * + * @since 2.7.0 + * + * @global + * + * @param {string} el Optional. Widget to fetch or none to update all. + * + * @return {void} + */ + window.ajaxPopulateWidgets = function(el) { + /** + * Fetch the latest representation of the widget via Ajax and show it. + * + * @param {number} i Number of half-seconds to use as the timeout. + * @param {string} id ID of the element which is going to be checked for changes. + * + * @return {void} + */ + function show(i, id) { + var p, e = $('#' + id + ' div.inside:visible').find('.widget-loading'); + // If the element is found in the dom, queue to load latest representation. + if ( e.length ) { + p = e.parent(); + setTimeout( function(){ + // Request the widget content. + p.load( ajaxurl + '?action=dashboard-widgets&widget=' + id + '&pagenow=' + pagenow, '', function() { + // Hide the parent and slide it out for visual fancyness. + p.hide().slideDown('normal', function(){ + $(this).css('display', ''); + }); + }); + }, i * 500 ); + } + } + + // If we have received a specific element to fetch, check if it is valid. + if ( el ) { + el = el.toString(); + // If the element is available as Ajax widget, show it. + if ( $.inArray(el, ajaxWidgets) !== -1 ) { + // Show element without any delay. + show(0, el); + } + } else { + // Walk through all ajaxWidgets, loading them after each other. + $.each( ajaxWidgets, show ); + } + }; + + // Initially populate ajax widgets. + ajaxPopulateWidgets(); + + // Register ajax widgets as postbox toggles. + postboxes.add_postbox_toggles(pagenow, { pbshow: ajaxPopulateWidgets } ); + + /** + * Control the Quick Press (Quick Draft) widget. + * + * @since 2.7.0 + * + * @global + * + * @return {void} + */ + window.quickPressLoad = function() { + var act = $('#quickpost-action'), t; + + // Enable the submit buttons. + $( '#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]' ).prop( 'disabled' , false ); + + t = $('#quick-press').on( 'submit', function( e ) { + e.preventDefault(); + + // Show a spinner. + $('#dashboard_quick_press #publishing-action .spinner').show(); + + // Disable the submit button to prevent duplicate submissions. + $('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop('disabled', true); + + // Post the entered data to save it. + $.post( t.attr( 'action' ), t.serializeArray(), function( data ) { + // Replace the form, and prepend the published post. + $('#dashboard_quick_press .inside').html( data ); + $('#quick-press').removeClass('initial-form'); + quickPressLoad(); + highlightLatestPost(); + + // Focus the title to allow for quickly drafting another post. + $('#title').trigger( 'focus' ); + }); + + /** + * Highlights the latest post for one second. + * + * @return {void} + */ + function highlightLatestPost () { + var latestPost = $('.drafts ul li').first(); + latestPost.css('background', '#fffbe5'); + setTimeout(function () { + latestPost.css('background', 'none'); + }, 1000); + } + } ); + + // Change the QuickPost action to the publish value. + $('#publish').on( 'click', function() { act.val( 'post-quickpress-publish' ); } ); + + $('#quick-press').on( 'click focusin', function() { + wpActiveEditor = 'content'; + }); + + autoResizeTextarea(); + }; + window.quickPressLoad(); + + // Enable the dragging functionality of the widgets. + $( '.meta-box-sortables' ).sortable( 'option', 'containment', '#wpwrap' ); + + /** + * Adjust the height of the textarea based on the content. + * + * @since 3.6.0 + * + * @return {void} + */ + function autoResizeTextarea() { + // When IE8 or older is used to render this document, exit. + if ( document.documentMode && document.documentMode < 9 ) { + return; + } + + // Add a hidden div. We'll copy over the text from the textarea to measure its height. + $('body').append( '<div class="quick-draft-textarea-clone" style="display: none;"></div>' ); + + var clone = $('.quick-draft-textarea-clone'), + editor = $('#content'), + editorHeight = editor.height(), + /* + * 100px roughly accounts for browser chrome and allows the + * save draft button to show on-screen at the same time. + */ + editorMaxHeight = $(window).height() - 100; + + /* + * Match up textarea and clone div as much as possible. + * Padding cannot be reliably retrieved using shorthand in all browsers. + */ + clone.css({ + 'font-family': editor.css('font-family'), + 'font-size': editor.css('font-size'), + 'line-height': editor.css('line-height'), + 'padding-bottom': editor.css('paddingBottom'), + 'padding-left': editor.css('paddingLeft'), + 'padding-right': editor.css('paddingRight'), + 'padding-top': editor.css('paddingTop'), + 'white-space': 'pre-wrap', + 'word-wrap': 'break-word', + 'display': 'none' + }); + + // The 'propertychange' is used in IE < 9. + editor.on('focus input propertychange', function() { + var $this = $(this), + // Add a non-breaking space to ensure that the height of a trailing newline is + // included. + textareaContent = $this.val() + ' ', + // Add 2px to compensate for border-top & border-bottom. + cloneHeight = clone.css('width', $this.css('width')).text(textareaContent).outerHeight() + 2; + + // Default to show a vertical scrollbar, if needed. + editor.css('overflow-y', 'auto'); + + // Only change the height if it has changed and both heights are below the max. + if ( cloneHeight === editorHeight || ( cloneHeight >= editorMaxHeight && editorHeight >= editorMaxHeight ) ) { + return; + } + + /* + * Don't allow editor to exceed the height of the window. + * This is also bound in CSS to a max-height of 1300px to be extra safe. + */ + if ( cloneHeight > editorMaxHeight ) { + editorHeight = editorMaxHeight; + } else { + editorHeight = cloneHeight; + } + + // Disable scrollbars because we adjust the height to the content. + editor.css('overflow', 'hidden'); + + $this.css('height', editorHeight + 'px'); + }); + } + +} ); + +jQuery( function( $ ) { + 'use strict'; + + var communityEventsData = window.communityEventsData, + dateI18n = wp.date.dateI18n, + format = wp.date.format, + sprintf = wp.i18n.sprintf, + __ = wp.i18n.__, + _x = wp.i18n._x, + app; + + /** + * Global Community Events namespace. + * + * @since 4.8.0 + * + * @memberOf wp + * @namespace wp.communityEvents + */ + app = window.wp.communityEvents = /** @lends wp.communityEvents */{ + initialized: false, + model: null, + + /** + * Initializes the wp.communityEvents object. + * + * @since 4.8.0 + * + * @return {void} + */ + init: function() { + if ( app.initialized ) { + return; + } + + var $container = $( '#community-events' ); + + /* + * When JavaScript is disabled, the errors container is shown, so + * that "This widget requires JavaScript" message can be seen. + * + * When JS is enabled, the container is hidden at first, and then + * revealed during the template rendering, if there actually are + * errors to show. + * + * The display indicator switches from `hide-if-js` to `aria-hidden` + * here in order to maintain consistency with all the other fields + * that key off of `aria-hidden` to determine their visibility. + * `aria-hidden` can't be used initially, because there would be no + * way to set it to false when JavaScript is disabled, which would + * prevent people from seeing the "This widget requires JavaScript" + * message. + */ + $( '.community-events-errors' ) + .attr( 'aria-hidden', 'true' ) + .removeClass( 'hide-if-js' ); + + $container.on( 'click', '.community-events-toggle-location, .community-events-cancel', app.toggleLocationForm ); + + /** + * Filters events based on entered location. + * + * @return {void} + */ + $container.on( 'submit', '.community-events-form', function( event ) { + var location = $( '#community-events-location' ).val().trim(); + + event.preventDefault(); + + /* + * Don't trigger a search if the search field is empty or the + * search term was made of only spaces before being trimmed. + */ + if ( ! location ) { + return; + } + + app.getEvents({ + location: location + }); + }); + + if ( communityEventsData && communityEventsData.cache && communityEventsData.cache.location && communityEventsData.cache.events ) { + app.renderEventsTemplate( communityEventsData.cache, 'app' ); + } else { + app.getEvents(); + } + + app.initialized = true; + }, + + /** + * Toggles the visibility of the Edit Location form. + * + * @since 4.8.0 + * + * @param {event|string} action 'show' or 'hide' to specify a state; + * or an event object to flip between states. + * + * @return {void} + */ + toggleLocationForm: function( action ) { + var $toggleButton = $( '.community-events-toggle-location' ), + $cancelButton = $( '.community-events-cancel' ), + $form = $( '.community-events-form' ), + $target = $(); + + if ( 'object' === typeof action ) { + // The action is the event object: get the clicked element. + $target = $( action.target ); + /* + * Strict comparison doesn't work in this case because sometimes + * we explicitly pass a string as value of aria-expanded and + * sometimes a boolean as the result of an evaluation. + */ + action = 'true' == $toggleButton.attr( 'aria-expanded' ) ? 'hide' : 'show'; + } + + if ( 'hide' === action ) { + $toggleButton.attr( 'aria-expanded', 'false' ); + $cancelButton.attr( 'aria-expanded', 'false' ); + $form.attr( 'aria-hidden', 'true' ); + /* + * If the Cancel button has been clicked, bring the focus back + * to the toggle button so users relying on screen readers don't + * lose their place. + */ + if ( $target.hasClass( 'community-events-cancel' ) ) { + $toggleButton.trigger( 'focus' ); + } + } else { + $toggleButton.attr( 'aria-expanded', 'true' ); + $cancelButton.attr( 'aria-expanded', 'true' ); + $form.attr( 'aria-hidden', 'false' ); + } + }, + + /** + * Sends REST API requests to fetch events for the widget. + * + * @since 4.8.0 + * + * @param {Object} requestParams REST API Request parameters object. + * + * @return {void} + */ + getEvents: function( requestParams ) { + var initiatedBy, + app = this, + $spinner = $( '.community-events-form' ).children( '.spinner' ); + + requestParams = requestParams || {}; + requestParams._wpnonce = communityEventsData.nonce; + requestParams.timezone = window.Intl ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : ''; + + initiatedBy = requestParams.location ? 'user' : 'app'; + + $spinner.addClass( 'is-active' ); + + wp.ajax.post( 'get-community-events', requestParams ) + .always( function() { + $spinner.removeClass( 'is-active' ); + }) + + .done( function( response ) { + if ( 'no_location_available' === response.error ) { + if ( requestParams.location ) { + response.unknownCity = requestParams.location; + } else { + /* + * No location was passed, which means that this was an automatic query + * based on IP, locale, and timezone. Since the user didn't initiate it, + * it should fail silently. Otherwise, the error could confuse and/or + * annoy them. + */ + delete response.error; + } + } + app.renderEventsTemplate( response, initiatedBy ); + }) + + .fail( function() { + app.renderEventsTemplate({ + 'location' : false, + 'events' : [], + 'error' : true + }, initiatedBy ); + }); + }, + + /** + * Renders the template for the Events section of the Events & News widget. + * + * @since 4.8.0 + * + * @param {Object} templateParams The various parameters that will get passed to wp.template. + * @param {string} initiatedBy 'user' to indicate that this was triggered manually by the user; + * 'app' to indicate it was triggered automatically by the app itself. + * + * @return {void} + */ + renderEventsTemplate: function( templateParams, initiatedBy ) { + var template, + elementVisibility, + $toggleButton = $( '.community-events-toggle-location' ), + $locationMessage = $( '#community-events-location-message' ), + $results = $( '.community-events-results' ); + + templateParams.events = app.populateDynamicEventFields( + templateParams.events, + communityEventsData.time_format + ); + + /* + * Hide all toggleable elements by default, to keep the logic simple. + * Otherwise, each block below would have to turn hide everything that + * could have been shown at an earlier point. + * + * The exception to that is that the .community-events container is hidden + * when the page is first loaded, because the content isn't ready yet, + * but once we've reached this point, it should always be shown. + */ + elementVisibility = { + '.community-events' : true, + '.community-events-loading' : false, + '.community-events-errors' : false, + '.community-events-error-occurred' : false, + '.community-events-could-not-locate' : false, + '#community-events-location-message' : false, + '.community-events-toggle-location' : false, + '.community-events-results' : false + }; + + /* + * Determine which templates should be rendered and which elements + * should be displayed. + */ + if ( templateParams.location.ip ) { + /* + * If the API determined the location by geolocating an IP, it will + * provide events, but not a specific location. + */ + $locationMessage.text( __( 'Attend an upcoming event near you.' ) ); + + if ( templateParams.events.length ) { + template = wp.template( 'community-events-event-list' ); + $results.html( template( templateParams ) ); + } else { + template = wp.template( 'community-events-no-upcoming-events' ); + $results.html( template( templateParams ) ); + } + + elementVisibility['#community-events-location-message'] = true; + elementVisibility['.community-events-toggle-location'] = true; + elementVisibility['.community-events-results'] = true; + + } else if ( templateParams.location.description ) { + template = wp.template( 'community-events-attend-event-near' ); + $locationMessage.html( template( templateParams ) ); + + if ( templateParams.events.length ) { + template = wp.template( 'community-events-event-list' ); + $results.html( template( templateParams ) ); + } else { + template = wp.template( 'community-events-no-upcoming-events' ); + $results.html( template( templateParams ) ); + } + + if ( 'user' === initiatedBy ) { + wp.a11y.speak( + sprintf( + /* translators: %s: The name of a city. */ + __( 'City updated. Listing events near %s.' ), + templateParams.location.description + ), + 'assertive' + ); + } + + elementVisibility['#community-events-location-message'] = true; + elementVisibility['.community-events-toggle-location'] = true; + elementVisibility['.community-events-results'] = true; + + } else if ( templateParams.unknownCity ) { + template = wp.template( 'community-events-could-not-locate' ); + $( '.community-events-could-not-locate' ).html( template( templateParams ) ); + wp.a11y.speak( + sprintf( + /* + * These specific examples were chosen to highlight the fact that a + * state is not needed, even for cities whose name is not unique. + * It would be too cumbersome to include that in the instructions + * to the user, so it's left as an implication. + */ + /* + * translators: %s is the name of the city we couldn't locate. + * Replace the examples with cities related to your locale. Test that + * they match the expected location and have upcoming events before + * including them. If no cities related to your locale have events, + * then use cities related to your locale that would be recognizable + * to most users. Use only the city name itself, without any region + * or country. Use the endonym (native locale name) instead of the + * English name if possible. + */ + __( 'We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ), + templateParams.unknownCity + ) + ); + + elementVisibility['.community-events-errors'] = true; + elementVisibility['.community-events-could-not-locate'] = true; + + } else if ( templateParams.error && 'user' === initiatedBy ) { + /* + * Errors messages are only shown for requests that were initiated + * by the user, not for ones that were initiated by the app itself. + * Showing error messages for an event that user isn't aware of + * could be confusing or unnecessarily distracting. + */ + wp.a11y.speak( __( 'An error occurred. Please try again.' ) ); + + elementVisibility['.community-events-errors'] = true; + elementVisibility['.community-events-error-occurred'] = true; + } else { + $locationMessage.text( __( 'Enter your closest city to find nearby events.' ) ); + + elementVisibility['#community-events-location-message'] = true; + elementVisibility['.community-events-toggle-location'] = true; + } + + // Set the visibility of toggleable elements. + _.each( elementVisibility, function( isVisible, element ) { + $( element ).attr( 'aria-hidden', ! isVisible ); + }); + + $toggleButton.attr( 'aria-expanded', elementVisibility['.community-events-toggle-location'] ); + + if ( templateParams.location && ( templateParams.location.ip || templateParams.location.latitude ) ) { + // Hide the form when there's a valid location. + app.toggleLocationForm( 'hide' ); + + if ( 'user' === initiatedBy ) { + /* + * When the form is programmatically hidden after a user search, + * bring the focus back to the toggle button so users relying + * on screen readers don't lose their place. + */ + $toggleButton.trigger( 'focus' ); + } + } else { + app.toggleLocationForm( 'show' ); + } + }, + + /** + * Populate event fields that have to be calculated on the fly. + * + * These can't be stored in the database, because they're dependent on + * the user's current time zone, locale, etc. + * + * @since 5.5.2 + * + * @param {Array} rawEvents The events that should have dynamic fields added to them. + * @param {string} timeFormat A time format acceptable by `wp.date.dateI18n()`. + * + * @returns {Array} + */ + populateDynamicEventFields: function( rawEvents, timeFormat ) { + // Clone the parameter to avoid mutating it, so that this can remain a pure function. + var populatedEvents = JSON.parse( JSON.stringify( rawEvents ) ); + + $.each( populatedEvents, function( index, event ) { + var timeZone = app.getTimeZone( event.start_unix_timestamp * 1000 ); + + event.user_formatted_date = app.getFormattedDate( + event.start_unix_timestamp * 1000, + event.end_unix_timestamp * 1000, + timeZone + ); + + event.user_formatted_time = dateI18n( + timeFormat, + event.start_unix_timestamp * 1000, + timeZone + ); + + event.timeZoneAbbreviation = app.getTimeZoneAbbreviation( event.start_unix_timestamp * 1000 ); + } ); + + return populatedEvents; + }, + + /** + * Returns the user's local/browser time zone, in a form suitable for `wp.date.i18n()`. + * + * @since 5.5.2 + * + * @param startTimestamp + * + * @returns {string|number} + */ + getTimeZone: function( startTimestamp ) { + /* + * Prefer a name like `Europe/Helsinki`, since that automatically tracks daylight savings. This + * doesn't need to take `startTimestamp` into account for that reason. + */ + var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + /* + * Fall back to an offset for IE11, which declares the property but doesn't assign a value. + */ + if ( 'undefined' === typeof timeZone ) { + /* + * It's important to use the _event_ time, not the _current_ + * time, so that daylight savings time is accounted for. + */ + timeZone = app.getFlippedTimeZoneOffset( startTimestamp ); + } + + return timeZone; + }, + + /** + * Get intuitive time zone offset. + * + * `Data.prototype.getTimezoneOffset()` returns a positive value for time zones + * that are _behind_ UTC, and a _negative_ value for ones that are ahead. + * + * See https://stackoverflow.com/questions/21102435/why-does-javascript-date-gettimezoneoffset-consider-0500-as-a-positive-off. + * + * @since 5.5.2 + * + * @param {number} startTimestamp + * + * @returns {number} + */ + getFlippedTimeZoneOffset: function( startTimestamp ) { + return new Date( startTimestamp ).getTimezoneOffset() * -1; + }, + + /** + * Get a short time zone name, like `PST`. + * + * @since 5.5.2 + * + * @param {number} startTimestamp + * + * @returns {string} + */ + getTimeZoneAbbreviation: function( startTimestamp ) { + var timeZoneAbbreviation, + eventDateTime = new Date( startTimestamp ); + + /* + * Leaving the `locales` argument undefined is important, so that the browser + * displays the abbreviation that's most appropriate for the current locale. For + * some that will be `UTC{+|-}{n}`, and for others it will be a code like `PST`. + * + * This doesn't need to take `startTimestamp` into account, because a name like + * `America/Chicago` automatically tracks daylight savings. + */ + var shortTimeStringParts = eventDateTime.toLocaleTimeString( undefined, { timeZoneName : 'short' } ).split( ' ' ); + + if ( 3 === shortTimeStringParts.length ) { + timeZoneAbbreviation = shortTimeStringParts[2]; + } + + if ( 'undefined' === typeof timeZoneAbbreviation ) { + /* + * It's important to use the _event_ time, not the _current_ + * time, so that daylight savings time is accounted for. + */ + var timeZoneOffset = app.getFlippedTimeZoneOffset( startTimestamp ), + sign = -1 === Math.sign( timeZoneOffset ) ? '' : '+'; + + // translators: Used as part of a string like `GMT+5` in the Events Widget. + timeZoneAbbreviation = _x( 'GMT', 'Events widget offset prefix' ) + sign + ( timeZoneOffset / 60 ); + } + + return timeZoneAbbreviation; + }, + + /** + * Format a start/end date in the user's local time zone and locale. + * + * @since 5.5.2 + * + * @param {int} startDate The Unix timestamp in milliseconds when the the event starts. + * @param {int} endDate The Unix timestamp in milliseconds when the the event ends. + * @param {string} timeZone A time zone string or offset which is parsable by `wp.date.i18n()`. + * + * @returns {string} + */ + getFormattedDate: function( startDate, endDate, timeZone ) { + var formattedDate; + + /* + * The `date_format` option is not used because it's important + * in this context to keep the day of the week in the displayed date, + * so that users can tell at a glance if the event is on a day they + * are available, without having to open the link. + * + * The case of crossing a year boundary is intentionally not handled. + * It's so rare in practice that it's not worth the complexity + * tradeoff. The _ending_ year should be passed to + * `multiple_month_event`, though, just in case. + */ + /* translators: Date format for upcoming events on the dashboard. Include the day of the week. See https://www.php.net/manual/datetime.format.php */ + var singleDayEvent = __( 'l, M j, Y' ), + /* translators: Date string for upcoming events. 1: Month, 2: Starting day, 3: Ending day, 4: Year. */ + multipleDayEvent = __( '%1$s %2$d–%3$d, %4$d' ), + /* translators: Date string for upcoming events. 1: Starting month, 2: Starting day, 3: Ending month, 4: Ending day, 5: Ending year. */ + multipleMonthEvent = __( '%1$s %2$d – %3$s %4$d, %5$d' ); + + // Detect single-day events. + if ( ! endDate || format( 'Y-m-d', startDate ) === format( 'Y-m-d', endDate ) ) { + formattedDate = dateI18n( singleDayEvent, startDate, timeZone ); + + // Multiple day events. + } else if ( format( 'Y-m', startDate ) === format( 'Y-m', endDate ) ) { + formattedDate = sprintf( + multipleDayEvent, + dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ), + dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone ) + ); + + // Multi-day events that cross a month boundary. + } else { + formattedDate = sprintf( + multipleMonthEvent, + dateI18n( _x( 'F', 'upcoming events month format' ), startDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), startDate, timeZone ), + dateI18n( _x( 'F', 'upcoming events month format' ), endDate, timeZone ), + dateI18n( _x( 'j', 'upcoming events day format' ), endDate, timeZone ), + dateI18n( _x( 'Y', 'upcoming events year format' ), endDate, timeZone ) + ); + } + + return formattedDate; + } + }; + + if ( $( '#dashboard_primary' ).is( ':visible' ) ) { + app.init(); + } else { + $( document ).on( 'postbox-toggled', function( event, postbox ) { + var $postbox = $( postbox ); + + if ( 'dashboard_primary' === $postbox.attr( 'id' ) && $postbox.is( ':visible' ) ) { + app.init(); + } + }); + } +}); + +/** + * Removed in 5.6.0, needed for back-compatibility. + * + * @since 4.8.0 + * @deprecated 5.6.0 + * + * @type {object} +*/ +window.communityEventsData.l10n = window.communityEventsData.l10n || { + enter_closest_city: '', + error_occurred_please_try_again: '', + attend_event_near_generic: '', + could_not_locate_city: '', + city_updated: '' +}; + +window.communityEventsData.l10n = window.wp.deprecateL10nObject( 'communityEventsData.l10n', window.communityEventsData.l10n, '5.6.0' ); diff --git a/wp-admin/js/dashboard.min.js b/wp-admin/js/dashboard.min.js new file mode 100644 index 0000000..34c5541 --- /dev/null +++ b/wp-admin/js/dashboard.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},window.communityEventsData=window.communityEventsData||{},jQuery(function(s){var t,n=s("#welcome-panel"),e=s("#wp_welcome_panel-hide");t=function(e){s.post(ajaxurl,{action:"update-welcome-panel",visible:e,welcomepanelnonce:s("#welcomepanelnonce").val()})},n.hasClass("hidden")&&e.prop("checked")&&n.removeClass("hidden"),s(".welcome-panel-close, .welcome-panel-dismiss a",n).on("click",function(e){e.preventDefault(),n.addClass("hidden"),t(0),s("#wp_welcome_panel-hide").prop("checked",!1)}),e.on("click",function(){n.toggleClass("hidden",!this.checked),t(this.checked?1:0)}),window.ajaxWidgets=["dashboard_primary"],window.ajaxPopulateWidgets=function(e){function t(e,t){var n,o=s("#"+t+" div.inside:visible").find(".widget-loading");o.length&&(n=o.parent(),setTimeout(function(){n.load(ajaxurl+"?action=dashboard-widgets&widget="+t+"&pagenow="+pagenow,"",function(){n.hide().slideDown("normal",function(){s(this).css("display","")})})},500*e))}e?(e=e.toString(),-1!==s.inArray(e,ajaxWidgets)&&t(0,e)):s.each(ajaxWidgets,t)},ajaxPopulateWidgets(),postboxes.add_postbox_toggles(pagenow,{pbshow:ajaxPopulateWidgets}),window.quickPressLoad=function(){var t,n,o,i,a,e=s("#quickpost-action");s('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop("disabled",!1),t=s("#quick-press").on("submit",function(e){e.preventDefault(),s("#dashboard_quick_press #publishing-action .spinner").show(),s('#quick-press .submit input[type="submit"], #quick-press .submit input[type="reset"]').prop("disabled",!0),s.post(t.attr("action"),t.serializeArray(),function(e){var t;s("#dashboard_quick_press .inside").html(e),s("#quick-press").removeClass("initial-form"),quickPressLoad(),(t=s(".drafts ul li").first()).css("background","#fffbe5"),setTimeout(function(){t.css("background","none")},1e3),s("#title").trigger("focus")})}),s("#publish").on("click",function(){e.val("post-quickpress-publish")}),s("#quick-press").on("click focusin",function(){wpActiveEditor="content"}),document.documentMode&&document.documentMode<9||(s("body").append('<div class="quick-draft-textarea-clone" style="display: none;"></div>'),n=s(".quick-draft-textarea-clone"),o=s("#content"),i=o.height(),a=s(window).height()-100,n.css({"font-family":o.css("font-family"),"font-size":o.css("font-size"),"line-height":o.css("line-height"),"padding-bottom":o.css("paddingBottom"),"padding-left":o.css("paddingLeft"),"padding-right":o.css("paddingRight"),"padding-top":o.css("paddingTop"),"white-space":"pre-wrap","word-wrap":"break-word",display:"none"}),o.on("focus input propertychange",function(){var e=s(this),t=e.val()+" ",t=n.css("width",e.css("width")).text(t).outerHeight()+2;o.css("overflow-y","auto"),t===i||a<=t&&a<=i||(i=a<t?a:t,o.css("overflow","hidden"),e.css("height",i+"px"))}))},window.quickPressLoad(),s(".meta-box-sortables").sortable("option","containment","#wpwrap")}),jQuery(function(c){"use strict";var r=window.communityEventsData,s=wp.date.dateI18n,m=wp.date.format,d=wp.i18n.sprintf,l=wp.i18n.__,u=wp.i18n._x,p=window.wp.communityEvents={initialized:!1,model:null,init:function(){var e;p.initialized||(e=c("#community-events"),c(".community-events-errors").attr("aria-hidden","true").removeClass("hide-if-js"),e.on("click",".community-events-toggle-location, .community-events-cancel",p.toggleLocationForm),e.on("submit",".community-events-form",function(e){var t=c("#community-events-location").val().trim();e.preventDefault(),t&&p.getEvents({location:t})}),r&&r.cache&&r.cache.location&&r.cache.events?p.renderEventsTemplate(r.cache,"app"):p.getEvents(),p.initialized=!0)},toggleLocationForm:function(e){var t=c(".community-events-toggle-location"),n=c(".community-events-cancel"),o=c(".community-events-form"),i=c();"object"==typeof e&&(i=c(e.target),e="true"==t.attr("aria-expanded")?"hide":"show"),"hide"===e?(t.attr("aria-expanded","false"),n.attr("aria-expanded","false"),o.attr("aria-hidden","true"),i.hasClass("community-events-cancel")&&t.trigger("focus")):(t.attr("aria-expanded","true"),n.attr("aria-expanded","true"),o.attr("aria-hidden","false"))},getEvents:function(t){var n,o=this,e=c(".community-events-form").children(".spinner");(t=t||{})._wpnonce=r.nonce,t.timezone=window.Intl?window.Intl.DateTimeFormat().resolvedOptions().timeZone:"",n=t.location?"user":"app",e.addClass("is-active"),wp.ajax.post("get-community-events",t).always(function(){e.removeClass("is-active")}).done(function(e){"no_location_available"===e.error&&(t.location?e.unknownCity=t.location:delete e.error),o.renderEventsTemplate(e,n)}).fail(function(){o.renderEventsTemplate({location:!1,events:[],error:!0},n)})},renderEventsTemplate:function(e,t){var n,o,i=c(".community-events-toggle-location"),a=c("#community-events-location-message"),s=c(".community-events-results");e.events=p.populateDynamicEventFields(e.events,r.time_format),o={".community-events":!0,".community-events-loading":!1,".community-events-errors":!1,".community-events-error-occurred":!1,".community-events-could-not-locate":!1,"#community-events-location-message":!1,".community-events-toggle-location":!1,".community-events-results":!1},e.location.ip?(a.text(l("Attend an upcoming event near you.")),n=e.events.length?wp.template("community-events-event-list"):wp.template("community-events-no-upcoming-events"),s.html(n(e)),o["#community-events-location-message"]=!0,o[".community-events-toggle-location"]=!0,o[".community-events-results"]=!0):e.location.description?(n=wp.template("community-events-attend-event-near"),a.html(n(e)),n=e.events.length?wp.template("community-events-event-list"):wp.template("community-events-no-upcoming-events"),s.html(n(e)),"user"===t&&wp.a11y.speak(d(l("City updated. Listing events near %s."),e.location.description),"assertive"),o["#community-events-location-message"]=!0,o[".community-events-toggle-location"]=!0,o[".community-events-results"]=!0):e.unknownCity?(n=wp.template("community-events-could-not-locate"),c(".community-events-could-not-locate").html(n(e)),wp.a11y.speak(d(l("We couldn\u2019t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland."),e.unknownCity)),o[".community-events-errors"]=!0,o[".community-events-could-not-locate"]=!0):e.error&&"user"===t?(wp.a11y.speak(l("An error occurred. Please try again.")),o[".community-events-errors"]=!0,o[".community-events-error-occurred"]=!0):(a.text(l("Enter your closest city to find nearby events.")),o["#community-events-location-message"]=!0,o[".community-events-toggle-location"]=!0),_.each(o,function(e,t){c(t).attr("aria-hidden",!e)}),i.attr("aria-expanded",o[".community-events-toggle-location"]),e.location&&(e.location.ip||e.location.latitude)?(p.toggleLocationForm("hide"),"user"===t&&i.trigger("focus")):p.toggleLocationForm("show")},populateDynamicEventFields:function(e,o){e=JSON.parse(JSON.stringify(e));return c.each(e,function(e,t){var n=p.getTimeZone(1e3*t.start_unix_timestamp);t.user_formatted_date=p.getFormattedDate(1e3*t.start_unix_timestamp,1e3*t.end_unix_timestamp,n),t.user_formatted_time=s(o,1e3*t.start_unix_timestamp,n),t.timeZoneAbbreviation=p.getTimeZoneAbbreviation(1e3*t.start_unix_timestamp)}),e},getTimeZone:function(e){var t=Intl.DateTimeFormat().resolvedOptions().timeZone;return t=void 0===t?p.getFlippedTimeZoneOffset(e):t},getFlippedTimeZoneOffset:function(e){return-1*new Date(e).getTimezoneOffset()},getTimeZoneAbbreviation:function(e){var t,n=new Date(e).toLocaleTimeString(void 0,{timeZoneName:"short"}).split(" ");return void 0===(t=3===n.length?n[2]:t)&&(n=p.getFlippedTimeZoneOffset(e),e=-1===Math.sign(n)?"":"+",t=u("GMT","Events widget offset prefix")+e+n/60),t},getFormattedDate:function(e,t,n){var o=l("l, M j, Y"),i=l("%1$s %2$d\u2013%3$d, %4$d"),a=l("%1$s %2$d \u2013 %3$s %4$d, %5$d"),i=t&&m("Y-m-d",e)!==m("Y-m-d",t)?m("Y-m",e)===m("Y-m",t)?d(i,s(u("F","upcoming events month format"),e,n),s(u("j","upcoming events day format"),e,n),s(u("j","upcoming events day format"),t,n),s(u("Y","upcoming events year format"),t,n)):d(a,s(u("F","upcoming events month format"),e,n),s(u("j","upcoming events day format"),e,n),s(u("F","upcoming events month format"),t,n),s(u("j","upcoming events day format"),t,n),s(u("Y","upcoming events year format"),t,n)):s(o,e,n);return i}};c("#dashboard_primary").is(":visible")?p.init():c(document).on("postbox-toggled",function(e,t){t=c(t);"dashboard_primary"===t.attr("id")&&t.is(":visible")&&p.init()})}),window.communityEventsData.l10n=window.communityEventsData.l10n||{enter_closest_city:"",error_occurred_please_try_again:"",attend_event_near_generic:"",could_not_locate_city:"",city_updated:""},window.communityEventsData.l10n=window.wp.deprecateL10nObject("communityEventsData.l10n",window.communityEventsData.l10n,"5.6.0");
\ No newline at end of file diff --git a/wp-admin/js/edit-comments.js b/wp-admin/js/edit-comments.js new file mode 100644 index 0000000..e7a9282 --- /dev/null +++ b/wp-admin/js/edit-comments.js @@ -0,0 +1,1356 @@ +/** + * 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 || $( '<div />' ); + 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 = $('<tr id="undo-' + id + '" class="undo un' + action + '" style="display:none;"><td colspan="' + n + '">' + note + '</td></tr>'); + } else { + author = $('.comment-author', el).text(); + h = $('<div id="undo-' + id + '" style="display:none;" class="undo un' + action + '">' + note + '</div>'); + } + + 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 ); + } + }, + + /** + * 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); diff --git a/wp-admin/js/edit-comments.min.js b/wp-admin/js/edit-comments.min.js new file mode 100644 index 0000000..f026b00 --- /dev/null +++ b/wp-admin/js/edit-comments.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(w){var o,s,i=document.title,C=w("#dashboard_right_now").length,c=wp.i18n.__,x=function(t){t=parseInt(t.html().replace(/[^0-9]+/g,""),10);return isNaN(t)?0:t},r=function(t,e){var n="";if(!isNaN(e)){if(3<(e=e<1?"0":e.toString()).length){for(;3<e.length;)n=thousandsSeparator+e.substr(e.length-3)+n,e=e.substr(0,e.length-3);e+=n}t.html(e)}},b=function(n,t){var e=".post-com-count-"+t,a="comment-count-no-comments",o="comment-count-approved";k("span.approved-count",n),t&&(t=w("span."+o,e),e=w("span."+a,e),t.each(function(){var t=w(this),e=x(t)+n;0===(e=e<1?0:e)?t.removeClass(o).addClass(a):t.addClass(o).removeClass(a),r(t,e)}),e.each(function(){var t=w(this);0<n?t.removeClass(a).addClass(o):t.addClass(a).removeClass(o),r(t,n)}))},k=function(t,n){w(t).each(function(){var t=w(this),e=x(t)+n;r(t,e=e<1?0:e)})},E=function(t){C&&t&&t.i18n_comments_text&&w(".comment-count a","#dashboard_right_now").text(t.i18n_comments_text)},R=function(t){t&&t.i18n_moderation_text&&(w(".comments-in-moderation-text").text(t.i18n_moderation_text),C)&&t.in_moderation&&w(".comment-mod-count","#dashboard_right_now")[0<t.in_moderation?"removeClass":"addClass"]("hidden")},l=function(t){var e,n,a;s=s||new RegExp(c("Comments (%s)").replace("%s","\\([0-9"+thousandsSeparator+"]+\\)")+"?"),o=o||w("<div />"),e=i,1<=(a=(a=s.exec(document.title))?(a=a[0],o.html(a),x(o)+t):(o.html(0),t))?(r(o,a),(n=s.exec(document.title))&&(e=document.title.replace(n[0],c("Comments (%s)").replace("%s",o.text())+" "))):(n=s.exec(e))&&(e=e.replace(n[0],c("Comments"))),document.title=e},I=function(n,t){var e=".post-com-count-"+t,a="comment-count-no-pending",o="post-com-count-no-pending",s="comment-count-pending";C||l(n),w("span.pending-count").each(function(){var t=w(this),e=x(t)+n;e<1&&(e=0),t.closest(".awaiting-mod")[0===e?"addClass":"removeClass"]("count-0"),r(t,e)}),t&&(t=w("span."+s,e),e=w("span."+a,e),t.each(function(){var t=w(this),e=x(t)+n;0===(e=e<1?0:e)?(t.parent().addClass(o),t.removeClass(s).addClass(a)):(t.parent().removeClass(o),t.addClass(s).removeClass(a)),r(t,e)}),e.each(function(){var t=w(this);0<n?(t.parent().removeClass(o),t.removeClass(a).addClass(s)):(t.parent().addClass(o),t.addClass(a).removeClass(s)),r(t,n)}))};window.setCommentsList=function(){var i,v=0,g=w('input[name="_total"]',"#comments-form"),l=w('input[name="_per_page"]',"#comments-form"),p=w('input[name="_page"]',"#comments-form"),_=function(t,e,n){e<v||(n&&(v=e),g.val(t.toString()))},t=function(t,e){var n,a,o,s=w("#"+e.element);!0!==e.parsed&&(o=e.parsed.responses[0]),a=w("#replyrow"),n=w("#comment_ID",a).val(),a=w("#replybtn",a),s.is(".unapproved")?(e.data.id==n&&a.text(c("Approve and Reply")),s.find(".row-actions span.view").addClass("hidden").end().find("div.comment_status").html("0")):(e.data.id==n&&a.text(c("Reply")),s.find(".row-actions span.view").removeClass("hidden").end().find("div.comment_status").html("1")),i=w("#"+e.element).is("."+e.dimClass)?1:-1,o?(E(o.supplemental),R(o.supplemental),I(i,o.supplemental.postId),b(-1*i,o.supplemental.postId)):(I(i),b(-1*i))},e=function(t,e){var n,a,o,s,i=!1,r=w(t.target).attr("data-wp-lists");return t.data._total=g.val()||0,t.data._per_page=l.val()||0,t.data._page=p.val()||0,t.data._url=document.location.href,t.data.comment_status=w('input[name="comment_status"]',"#comments-form").val(),-1!=r.indexOf(":trash=1")?i="trash":-1!=r.indexOf(":spam=1")&&(i="spam"),i&&(n=r.replace(/.*?comment-([0-9]+).*/,"$1"),r=w("#comment-"+n),o=w("#"+i+"-undo-holder").html(),r.find(".check-column :checkbox").prop("checked",!1),r.siblings("#replyrow").length&&commentReply.cid==n&&commentReply.close(),a=r.is("tr")?(a=r.children(":visible").length,s=w(".author strong",r).text(),w('<tr id="undo-'+n+'" class="undo un'+i+'" style="display:none;"><td colspan="'+a+'">'+o+"</td></tr>")):(s=w(".comment-author",r).text(),w('<div id="undo-'+n+'" style="display:none;" class="undo un'+i+'">'+o+"</div>")),r.before(a),w("strong","#undo-"+n).text(s),(o=w(".undo a","#undo-"+n)).attr("href","comment.php?action=un"+i+"comment&c="+n+"&_wpnonce="+t.data._ajax_nonce),o.attr("data-wp-lists","delete:the-comment-list:comment-"+n+"::un"+i+"=1"),o.attr("class","vim-z vim-destructive aria-button-if-js"),w(".avatar",r).first().clone().prependTo("#undo-"+n+" ."+i+"-undo-inside"),o.on("click",function(t){t.preventDefault(),t.stopPropagation(),e.wpList.del(this),w("#undo-"+n).css({backgroundColor:"#ceb"}).fadeOut(350,function(){w(this).remove(),w("#comment-"+n).css("backgroundColor","").fadeIn(300,function(){w(this).show()})})})),t},n=function(t,e){var n,a,o,s,i=!0===e.parsed?{}:e.parsed.responses[0],r=!0===e.parsed?"":i.supplemental.status,l=!0===e.parsed?"":i.supplemental.postId,p=!0===e.parsed?"":i.supplemental,c=w(e.target).parent(),d=w("#"+e.element),m=d.hasClass("approved")&&!d.hasClass("unapproved"),u=d.hasClass("unapproved"),h=d.hasClass("spam"),d=d.hasClass("trash"),f=!1;E(p),R(p),c.is("span.undo")?(c.hasClass("unspam")?(n=-1,"trash"===r?a=1:"1"===r?s=1:"0"===r&&(o=1)):c.hasClass("untrash")&&(a=-1,"spam"===r?n=1:"1"===r?s=1:"0"===r&&(o=1)),f=!0):c.is("span.spam")?(m?s=-1:u?o=-1:d&&(a=-1),n=1):c.is("span.unspam")?(m?o=1:u?s=1:(d||h)&&(c.hasClass("approve")?s=1:c.hasClass("unapprove")&&(o=1)),n=-1):c.is("span.trash")?(m?s=-1:u?o=-1:h&&(n=-1),a=1):c.is("span.untrash")?(m?o=1:u?s=1:d&&(c.hasClass("approve")?s=1:c.hasClass("unapprove")&&(o=1)),a=-1):c.is("span.approve:not(.unspam):not(.untrash)")?o=-(s=1):c.is("span.unapprove:not(.unspam):not(.untrash)")?(s=-1,o=1):c.is("span.delete")&&(h?n=-1:d&&(a=-1)),o&&(I(o,l),k("span.all-count",o)),s&&(b(s,l),k("span.all-count",s)),n&&k("span.spam-count",n),a&&k("span.trash-count",a),("trash"===e.data.comment_status&&!x(w("span.trash-count"))||"spam"===e.data.comment_status&&!x(w("span.spam-count")))&&w("#delete_all").hide(),C||(p=g.val()?parseInt(g.val(),10):0,w(e.target).parent().is("span.undo")?p++:p--,p<0&&(p=0),"object"==typeof t?i.supplemental.total_items_i18n&&v<i.supplemental.time?((r=i.supplemental.total_items_i18n||"")&&(w(".displaying-num").text(r.replace(" ",String.fromCharCode(160))),w(".total-pages").text(i.supplemental.total_pages_i18n.replace(" ",String.fromCharCode(160))),w(".tablenav-pages").find(".next-page, .last-page").toggleClass("disabled",i.supplemental.total_pages==w(".current-page").val())),_(p,i.supplemental.time,!0)):i.supplemental.time&&_(p,i.supplemental.time,!1):_(p,t,!1)),theExtraList&&0!==theExtraList.length&&0!==theExtraList.children().length&&!f&&(theList.get(0).wpList.add(theExtraList.children(":eq(0):not(.no-items)").remove().clone()),y(),m=function(){w("#the-comment-list tr:visible").length||theList.get(0).wpList.add(theExtraList.find(".no-items").clone())},(u=w(":animated","#the-comment-list")).length?u.promise().done(m):m())},y=function(t){var e=w.query.get(),n=w(".total-pages").text(),a=w('input[name="_per_page"]',"#comments-form").val();e.paged||(e.paged=1),e.paged>n||(t?(theExtraList.empty(),e.number=Math.min(8,a)):(e.number=1,e.offset=Math.min(8,a)-1),e.no_placeholder=!0,e.paged++,!0===e.comment_type&&(e.comment_type=""),e=w.extend(e,{action:"fetch-list",list_args:list_args,_ajax_fetch_list_nonce:w("#_ajax_fetch_list_nonce").val()}),w.ajax({url:ajaxurl,global:!1,dataType:"json",data:e,success:function(t){theExtraList.get(0).wpList.add(t.rows)}}))};window.theExtraList=w("#the-extra-comment-list").wpList({alt:"",delColor:"none",addColor:"none"}),window.theList=w("#the-comment-list").wpList({alt:"",delBefore:e,dimAfter:t,delAfter:n,addColor:"none"}).on("wpListDelEnd",function(t,e){var n=w(e.target).attr("data-wp-lists"),e=e.element.replace(/[^0-9]+/g,"");-1==n.indexOf(":trash=1")&&-1==n.indexOf(":spam=1")||w("#undo-"+e).fadeIn(300,function(){w(this).show()})})},window.commentReply={cid:"",act:"",originalContent:"",init:function(){var t=w("#replyrow");w(".cancel",t).on("click",function(){return commentReply.revert()}),w(".save",t).on("click",function(){return commentReply.send()}),w("input#author-name, input#author-email, input#author-url",t).on("keypress",function(t){if(13==t.which)return commentReply.send(),t.preventDefault(),!1}),w("#the-comment-list .column-comment > p").on("dblclick",function(){commentReply.toggle(w(this).parent())}),w("#doaction, #post-query-submit").on("click",function(){0<w("#the-comment-list #replyrow").length&&commentReply.close()}),this.comments_listing=w('#comments-form > input[name="comment_status"]').val()||""},addEvents:function(t){t.each(function(){w(this).find(".column-comment > p").on("dblclick",function(){commentReply.toggle(w(this).parent())})})},toggle:function(t){"none"!==w(t).css("display")&&(w("#replyrow").parent().is("#com-reply")||window.confirm(c("Are you sure you want to edit this comment?\nThe changes you made will be lost.")))&&w(t).find("button.vim-q").trigger("click")},revert:function(){if(w("#the-comment-list #replyrow").length<1)return!1;w("#replyrow").fadeOut("fast",function(){commentReply.close()})},close:function(){var t=w(),e=w("#replyrow");e.parent().is("#com-reply")||(this.cid&&(t=w("#comment-"+this.cid)),"edit-comment"===this.act&&t.fadeIn(300,function(){t.show().find(".vim-q").attr("aria-expanded","false").trigger("focus")}).css("backgroundColor",""),"replyto-comment"===this.act&&t.find(".vim-r").attr("aria-expanded","false").trigger("focus"),"undefined"!=typeof QTags&&QTags.closeAllTags("replycontent"),w("#add-new-comment").css("display",""),e.hide(),w("#com-reply").append(e),w("#replycontent").css("height","").val(""),w("#edithead input").val(""),w(".notice-error",e).addClass("hidden").find(".error").empty(),w(".spinner",e).removeClass("is-active"),this.cid="",this.originalContent="")},open:function(t,e,n){var a,o,s,i,r=w("#comment-"+t),l=r.height();return this.discardCommentChanges()&&(this.close(),this.cid=t,a=w("#replyrow"),o=w("#inline-"+t),s=this.act=("edit"==(n=n||"replyto")?"edit":"replyto")+"-comment",this.originalContent=w("textarea.comment",o).val(),i=w("> th:visible, > td:visible",r).length,a.hasClass("inline-edit-row")&&0!==i&&w("td",a).attr("colspan",i),w("#action",a).val(s),w("#comment_post_ID",a).val(e),w("#comment_ID",a).val(t),"edit"==n?(w("#author-name",a).val(w("div.author",o).text()),w("#author-email",a).val(w("div.author-email",o).text()),w("#author-url",a).val(w("div.author-url",o).text()),w("#status",a).val(w("div.comment_status",o).text()),w("#replycontent",a).val(w("textarea.comment",o).val()),w("#edithead, #editlegend, #savebtn",a).show(),w("#replyhead, #replybtn, #addhead, #addbtn",a).hide(),120<l&&(i=500<l?500:l,w("#replycontent",a).css("height",i+"px")),r.after(a).fadeOut("fast",function(){w("#replyrow").fadeIn(300,function(){w(this).show()})})):"add"==n?(w("#addhead, #addbtn",a).show(),w("#replyhead, #replybtn, #edithead, #editlegend, #savebtn",a).hide(),w("#the-comment-list").prepend(a),w("#replyrow").fadeIn(300)):(s=w("#replybtn",a),w("#edithead, #editlegend, #savebtn, #addhead, #addbtn",a).hide(),w("#replyhead, #replybtn",a).show(),r.after(a),r.hasClass("unapproved")?s.text(c("Approve and Reply")):s.text(c("Reply")),w("#replyrow").fadeIn(300,function(){w(this).show()})),setTimeout(function(){var e=!1,t=w("#replyrow").offset().top,n=t+w("#replyrow").height(),a=window.pageYOffset||document.documentElement.scrollTop,o=document.documentElement.clientHeight||window.innerHeight||0;a+o-20<n?window.scroll(0,n-o+35):t-20<a&&window.scroll(0,t-35),w("#replycontent").trigger("focus").on("keyup",function(t){27!==t.which||e||commentReply.revert()}).on("compositionstart",function(){e=!0})},600)),!1},send:function(){var e={};w("#replysubmit .error-notice").addClass("hidden"),w("#replysubmit .spinner").addClass("is-active"),w("#replyrow input").not(":button").each(function(){var t=w(this);e[t.attr("name")]=t.val()}),e.content=w("#replycontent").val(),e.id=e.comment_post_ID,e.comments_listing=this.comments_listing,e.p=w('[name="p"]').val(),w("#comment-"+w("#comment_ID").val()).hasClass("unapproved")&&(e.approve_parent=1),w.ajax({type:"POST",url:ajaxurl,data:e,success:function(t){commentReply.show(t)},error:function(t){commentReply.error(t)}})},show:function(t){var e,n,a,o=this;return"string"==typeof t?(o.error({responseText:t}),!1):(t=wpAjax.parseAjaxResponse(t)).errors?(o.error({responseText:wpAjax.broken}),!1):(o.revert(),e="#comment-"+(t=t.responses[0]).id,"edit-comment"==o.act&&w(e).remove(),void(t.supplemental.parent_approved&&(a=w("#comment-"+t.supplemental.parent_approved),I(-1,t.supplemental.parent_post_id),"moderated"==this.comments_listing)?a.animate({backgroundColor:"#CCEEBB"},400,function(){a.fadeOut()}):(t.supplemental.i18n_comments_text&&(E(t.supplemental),R(t.supplemental),b(1,t.supplemental.parent_post_id),k("span.all-count",1)),t.data=t.data||"",t=t.data.toString().trim(),w(t).hide(),w("#replyrow").after(t),e=w(e),o.addEvents(e),n=e.hasClass("unapproved")?"#FFFFE0":e.closest(".widefat, .postbox").css("backgroundColor"),e.animate({backgroundColor:"#CCEEBB"},300).animate({backgroundColor:n},300,function(){a&&a.length&&a.animate({backgroundColor:"#CCEEBB"},300).animate({backgroundColor:n},300).removeClass("unapproved").addClass("approved").find("div.comment_status").html("1")}))))},error:function(t){var e=t.statusText,n=w("#replysubmit .notice-error"),a=n.find(".error");w("#replysubmit .spinner").removeClass("is-active"),(e=t.responseText?t.responseText.replace(/<.[^<>]*?>/g,""):e)&&(n.removeClass("hidden"),a.html(e))},addcomment:function(t){var e=this;w("#add-new-comment").fadeOut(200,function(){e.open(0,t,"add"),w("table.comments-box").css("display",""),w("#no-comments").remove()})},discardCommentChanges:function(){var t=w("#replyrow");return""===w("#replycontent",t).val()||this.originalContent===w("#replycontent",t).val()||window.confirm(c("Are you sure you want to do this?\nThe comment changes you made will be lost."))}},w(function(){var t,e,n,a;setCommentsList(),commentReply.init(),w(document).on("click","span.delete a.delete",function(t){t.preventDefault()}),void 0!==w.table_hotkeys&&(t=function(n){return function(){var t="next"==n?"first":"last",e=w(".tablenav-pages ."+n+"-page:not(.disabled)");e.length&&(window.location=e[0].href.replace(/\&hotkeys_highlight_(first|last)=1/g,"")+"&hotkeys_highlight_"+t+"=1")}},e=function(t,e){window.location=w("span.edit a",e).attr("href")},n=function(){w("#cb-select-all-1").data("wp-toggle",1).trigger("click").removeData("wp-toggle")},a=function(e){return function(){var t=w('select[name="action"]');w('option[value="'+e+'"]',t).prop("selected",!0),w("#doaction").trigger("click")}},w.table_hotkeys(w("table.widefat"),["a","u","s","d","r","q","z",["e",e],["shift+x",n],["shift+a",a("approve")],["shift+s",a("spam")],["shift+d",a("delete")],["shift+t",a("trash")],["shift+z",a("untrash")],["shift+u",a("unapprove")]],{highlight_first:adminCommentsSettings.hotkeys_highlight_first,highlight_last:adminCommentsSettings.hotkeys_highlight_last,prev_page_link_cb:t("prev"),next_page_link_cb:t("next"),hotkeys_opts:{disableInInput:!0,type:"keypress",noDisable:'.check-column input[type="checkbox"]'},cycle_expr:"#the-comment-list tr",start_row_index:0})),w("#the-comment-list").on("click",".comment-inline",function(){var t=w(this),e="replyto";void 0!==t.data("action")&&(e=t.data("action")),w(this).attr("aria-expanded","true"),commentReply.open(t.data("commentId"),t.data("postId"),e)})})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/editor-expand.js b/wp-admin/js/editor-expand.js new file mode 100644 index 0000000..a47c548 --- /dev/null +++ b/wp-admin/js/editor-expand.js @@ -0,0 +1,1617 @@ +/** + * @output wp-admin/js/editor-expand.js + */ + +( function( window, $, undefined ) { + 'use strict'; + + var $window = $( window ), + $document = $( document ), + $adminBar = $( '#wpadminbar' ), + $footer = $( '#wpfooter' ); + + /** + * Handles the resizing of the editor. + * + * @since 4.0.0 + * + * @return {void} + */ + $( function() { + var $wrap = $( '#postdivrich' ), + $contentWrap = $( '#wp-content-wrap' ), + $tools = $( '#wp-content-editor-tools' ), + $visualTop = $(), + $visualEditor = $(), + $textTop = $( '#ed_toolbar' ), + $textEditor = $( '#content' ), + textEditor = $textEditor[0], + oldTextLength = 0, + $bottom = $( '#post-status-info' ), + $menuBar = $(), + $statusBar = $(), + $sideSortables = $( '#side-sortables' ), + $postboxContainer = $( '#postbox-container-1' ), + $postBody = $('#post-body'), + fullscreen = window.wp.editor && window.wp.editor.fullscreen, + mceEditor, + mceBind = function(){}, + mceUnbind = function(){}, + fixedTop = false, + fixedBottom = false, + fixedSideTop = false, + fixedSideBottom = false, + scrollTimer, + lastScrollPosition = 0, + pageYOffsetAtTop = 130, + pinnedToolsTop = 56, + sidebarBottom = 20, + autoresizeMinHeight = 300, + initialMode = $contentWrap.hasClass( 'tmce-active' ) ? 'tinymce' : 'html', + advanced = !! parseInt( window.getUserSetting( 'hidetb' ), 10 ), + // These are corrected when adjust() runs, except on scrolling if already set. + heights = { + windowHeight: 0, + windowWidth: 0, + adminBarHeight: 0, + toolsHeight: 0, + menuBarHeight: 0, + visualTopHeight: 0, + textTopHeight: 0, + bottomHeight: 0, + statusBarHeight: 0, + sideSortablesHeight: 0 + }; + + /** + * Resizes textarea based on scroll height and width. + * + * Doesn't shrink the editor size below the 300px auto resize minimum height. + * + * @since 4.6.1 + * + * @return {void} + */ + var shrinkTextarea = window._.throttle( function() { + var x = window.scrollX || document.documentElement.scrollLeft; + var y = window.scrollY || document.documentElement.scrollTop; + var height = parseInt( textEditor.style.height, 10 ); + + textEditor.style.height = autoresizeMinHeight + 'px'; + + if ( textEditor.scrollHeight > autoresizeMinHeight ) { + textEditor.style.height = textEditor.scrollHeight + 'px'; + } + + if ( typeof x !== 'undefined' ) { + window.scrollTo( x, y ); + } + + if ( textEditor.scrollHeight < height ) { + adjust(); + } + }, 300 ); + + /** + * Resizes the text editor depending on the old text length. + * + * If there is an mceEditor and it is hidden, it resizes the editor depending + * on the old text length. If the current length of the text is smaller than + * the old text length, it shrinks the text area. Otherwise it resizes the editor to + * the scroll height. + * + * @since 4.6.1 + * + * @return {void} + */ + function textEditorResize() { + var length = textEditor.value.length; + + if ( mceEditor && ! mceEditor.isHidden() ) { + return; + } + + if ( ! mceEditor && initialMode === 'tinymce' ) { + return; + } + + if ( length < oldTextLength ) { + shrinkTextarea(); + } else if ( parseInt( textEditor.style.height, 10 ) < textEditor.scrollHeight ) { + textEditor.style.height = Math.ceil( textEditor.scrollHeight ) + 'px'; + adjust(); + } + + oldTextLength = length; + } + + /** + * Gets the height and widths of elements. + * + * Gets the heights of the window, the adminbar, the tools, the menu, + * the visualTop, the textTop, the bottom, the statusbar and sideSortables + * and stores these in the heights object. Defaults to 0. + * Gets the width of the window and stores this in the heights object. + * + * @since 4.0.0 + * + * @return {void} + */ + function getHeights() { + var windowWidth = $window.width(); + + heights = { + windowHeight: $window.height(), + windowWidth: windowWidth, + adminBarHeight: ( windowWidth > 600 ? $adminBar.outerHeight() : 0 ), + toolsHeight: $tools.outerHeight() || 0, + menuBarHeight: $menuBar.outerHeight() || 0, + visualTopHeight: $visualTop.outerHeight() || 0, + textTopHeight: $textTop.outerHeight() || 0, + bottomHeight: $bottom.outerHeight() || 0, + statusBarHeight: $statusBar.outerHeight() || 0, + sideSortablesHeight: $sideSortables.height() || 0 + }; + + // Adjust for hidden menubar. + if ( heights.menuBarHeight < 3 ) { + heights.menuBarHeight = 0; + } + } + + // We need to wait for TinyMCE to initialize. + /** + * Binds all necessary functions for editor expand to the editor when the editor + * is initialized. + * + * @since 4.0.0 + * + * @param {event} event The TinyMCE editor init event. + * @param {object} editor The editor to bind the vents on. + * + * @return {void} + */ + $document.on( 'tinymce-editor-init.editor-expand', function( event, editor ) { + // VK contains the type of key pressed. VK = virtual keyboard. + var VK = window.tinymce.util.VK, + /** + * Hides any float panel with a hover state. Additionally hides tooltips. + * + * @return {void} + */ + hideFloatPanels = _.debounce( function() { + ! $( '.mce-floatpanel:hover' ).length && window.tinymce.ui.FloatPanel.hideAll(); + $( '.mce-tooltip' ).hide(); + }, 1000, true ); + + // Make sure it's the main editor. + if ( editor.id !== 'content' ) { + return; + } + + // Copy the editor instance. + mceEditor = editor; + + // Set the minimum height to the initial viewport height. + editor.settings.autoresize_min_height = autoresizeMinHeight; + + // Get the necessary UI elements. + $visualTop = $contentWrap.find( '.mce-toolbar-grp' ); + $visualEditor = $contentWrap.find( '.mce-edit-area' ); + $statusBar = $contentWrap.find( '.mce-statusbar' ); + $menuBar = $contentWrap.find( '.mce-menubar' ); + + /** + * Gets the offset of the editor. + * + * @return {number|boolean} Returns the offset of the editor + * or false if there is no offset height. + */ + function mceGetCursorOffset() { + var node = editor.selection.getNode(), + range, view, offset; + + /* + * If editor.wp.getView and the selection node from the editor selection + * are defined, use this as a view for the offset. + */ + if ( editor.wp && editor.wp.getView && ( view = editor.wp.getView( node ) ) ) { + offset = view.getBoundingClientRect(); + } else { + range = editor.selection.getRng(); + + // Try to get the offset from a range. + try { + offset = range.getClientRects()[0]; + } catch( er ) {} + + // Get the offset from the bounding client rectangle of the node. + if ( ! offset ) { + offset = node.getBoundingClientRect(); + } + } + + return offset.height ? offset : false; + } + + /** + * Filters the special keys that should not be used for scrolling. + * + * @since 4.0.0 + * + * @param {event} event The event to get the key code from. + * + * @return {void} + */ + function mceKeyup( event ) { + var key = event.keyCode; + + // Bail on special keys. Key code 47 is a '/'. + if ( key <= 47 && ! ( key === VK.SPACEBAR || key === VK.ENTER || key === VK.DELETE || key === VK.BACKSPACE || key === VK.UP || key === VK.LEFT || key === VK.DOWN || key === VK.UP ) ) { + return; + // OS keys, function keys, num lock, scroll lock. Key code 91-93 are OS keys. + // Key code 112-123 are F1 to F12. Key code 144 is num lock. Key code 145 is scroll lock. + } else if ( ( key >= 91 && key <= 93 ) || ( key >= 112 && key <= 123 ) || key === 144 || key === 145 ) { + return; + } + + mceScroll( key ); + } + + /** + * Makes sure the cursor is always visible in the editor. + * + * Makes sure the cursor is kept between the toolbars of the editor and scrolls + * the window when the cursor moves out of the viewport to a wpview. + * Setting a buffer > 0 will prevent the browser default. + * Some browsers will scroll to the middle, + * others to the top/bottom of the *window* when moving the cursor out of the viewport. + * + * @since 4.1.0 + * + * @param {string} key The key code of the pressed key. + * + * @return {void} + */ + function mceScroll( key ) { + var offset = mceGetCursorOffset(), + buffer = 50, + cursorTop, cursorBottom, editorTop, editorBottom; + + // Don't scroll if there is no offset. + if ( ! offset ) { + return; + } + + // Determine the cursorTop based on the offset and the top of the editor iframe. + cursorTop = offset.top + editor.iframeElement.getBoundingClientRect().top; + + // Determine the cursorBottom based on the cursorTop and offset height. + cursorBottom = cursorTop + offset.height; + + // Subtract the buffer from the cursorTop. + cursorTop = cursorTop - buffer; + + // Add the buffer to the cursorBottom. + cursorBottom = cursorBottom + buffer; + editorTop = heights.adminBarHeight + heights.toolsHeight + heights.menuBarHeight + heights.visualTopHeight; + + /* + * Set the editorBottom based on the window Height, and add the bottomHeight and statusBarHeight if the + * advanced editor is enabled. + */ + editorBottom = heights.windowHeight - ( advanced ? heights.bottomHeight + heights.statusBarHeight : 0 ); + + // Don't scroll if the node is taller than the visible part of the editor. + if ( editorBottom - editorTop < offset.height ) { + return; + } + + /* + * If the cursorTop is smaller than the editorTop and the up, left + * or backspace key is pressed, scroll the editor to the position defined + * by the cursorTop, pageYOffset and editorTop. + */ + if ( cursorTop < editorTop && ( key === VK.UP || key === VK.LEFT || key === VK.BACKSPACE ) ) { + window.scrollTo( window.pageXOffset, cursorTop + window.pageYOffset - editorTop ); + + /* + * If any other key is pressed or the cursorTop is bigger than the editorTop, + * scroll the editor to the position defined by the cursorBottom, + * pageYOffset and editorBottom. + */ + } else if ( cursorBottom > editorBottom ) { + window.scrollTo( window.pageXOffset, cursorBottom + window.pageYOffset - editorBottom ); + } + } + + /** + * If the editor is fullscreen, calls adjust. + * + * @since 4.1.0 + * + * @param {event} event The FullscreenStateChanged event. + * + * @return {void} + */ + function mceFullscreenToggled( event ) { + // event.state is true if the editor is fullscreen. + if ( ! event.state ) { + adjust(); + } + } + + /** + * Shows the editor when scrolled. + * + * Binds the hideFloatPanels function on the window scroll.mce-float-panels event. + * Executes the wpAutoResize on the active editor. + * + * @since 4.0.0 + * + * @return {void} + */ + function mceShow() { + $window.on( 'scroll.mce-float-panels', hideFloatPanels ); + + setTimeout( function() { + editor.execCommand( 'wpAutoResize' ); + adjust(); + }, 300 ); + } + + /** + * Resizes the editor. + * + * Removes all functions from the window scroll.mce-float-panels event. + * Resizes the text editor and scrolls to a position based on the pageXOffset and adminBarHeight. + * + * @since 4.0.0 + * + * @return {void} + */ + function mceHide() { + $window.off( 'scroll.mce-float-panels' ); + + setTimeout( function() { + var top = $contentWrap.offset().top; + + if ( window.pageYOffset > top ) { + window.scrollTo( window.pageXOffset, top - heights.adminBarHeight ); + } + + textEditorResize(); + adjust(); + }, 100 ); + + adjust(); + } + + /** + * Toggles advanced states. + * + * @since 4.1.0 + * + * @return {void} + */ + function toggleAdvanced() { + advanced = ! advanced; + } + + /** + * Binds events of the editor and window. + * + * @since 4.0.0 + * + * @return {void} + */ + mceBind = function() { + editor.on( 'keyup', mceKeyup ); + editor.on( 'show', mceShow ); + editor.on( 'hide', mceHide ); + editor.on( 'wp-toolbar-toggle', toggleAdvanced ); + + // Adjust when the editor resizes. + editor.on( 'setcontent wp-autoresize wp-toolbar-toggle', adjust ); + + // Don't hide the caret after undo/redo. + editor.on( 'undo redo', mceScroll ); + + // Adjust when exiting TinyMCE's fullscreen mode. + editor.on( 'FullscreenStateChanged', mceFullscreenToggled ); + + $window.off( 'scroll.mce-float-panels' ).on( 'scroll.mce-float-panels', hideFloatPanels ); + }; + + /** + * Unbinds the events of the editor and window. + * + * @since 4.0.0 + * + * @return {void} + */ + mceUnbind = function() { + editor.off( 'keyup', mceKeyup ); + editor.off( 'show', mceShow ); + editor.off( 'hide', mceHide ); + editor.off( 'wp-toolbar-toggle', toggleAdvanced ); + editor.off( 'setcontent wp-autoresize wp-toolbar-toggle', adjust ); + editor.off( 'undo redo', mceScroll ); + editor.off( 'FullscreenStateChanged', mceFullscreenToggled ); + + $window.off( 'scroll.mce-float-panels' ); + }; + + if ( $wrap.hasClass( 'wp-editor-expand' ) ) { + + // Adjust "immediately". + mceBind(); + initialResize( adjust ); + } + } ); + + /** + * Adjusts the toolbars heights and positions. + * + * Adjusts the toolbars heights and positions based on the scroll position on + * the page, the active editor mode and the heights of the editor, admin bar and + * side bar. + * + * @since 4.0.0 + * + * @param {event} event The event that calls this function. + * + * @return {void} + */ + function adjust( event ) { + + // Makes sure we're not in fullscreen mode. + if ( fullscreen && fullscreen.settings.visible ) { + return; + } + + var windowPos = $window.scrollTop(), + type = event && event.type, + resize = type !== 'scroll', + visual = mceEditor && ! mceEditor.isHidden(), + buffer = autoresizeMinHeight, + postBodyTop = $postBody.offset().top, + borderWidth = 1, + contentWrapWidth = $contentWrap.width(), + $top, $editor, sidebarTop, footerTop, canPin, + topPos, topHeight, editorPos, editorHeight; + + /* + * Refresh the heights if type isn't 'scroll' + * or heights.windowHeight isn't set. + */ + if ( resize || ! heights.windowHeight ) { + getHeights(); + } + + // Resize on resize event when the editor is in text mode. + if ( ! visual && type === 'resize' ) { + textEditorResize(); + } + + if ( visual ) { + $top = $visualTop; + $editor = $visualEditor; + topHeight = heights.visualTopHeight; + } else { + $top = $textTop; + $editor = $textEditor; + topHeight = heights.textTopHeight; + } + + // Return if TinyMCE is still initializing. + if ( ! visual && ! $top.length ) { + return; + } + + topPos = $top.parent().offset().top; + editorPos = $editor.offset().top; + editorHeight = $editor.outerHeight(); + + /* + * If in visual mode, checks if the editorHeight is greater than the autoresizeMinHeight + topHeight. + * If not in visual mode, checks if the editorHeight is greater than the autoresizeMinHeight + 20. + */ + canPin = visual ? autoresizeMinHeight + topHeight : autoresizeMinHeight + 20; // 20px from textarea padding. + canPin = editorHeight > ( canPin + 5 ); + + if ( ! canPin ) { + if ( resize ) { + $tools.css( { + position: 'absolute', + top: 0, + width: contentWrapWidth + } ); + + if ( visual && $menuBar.length ) { + $menuBar.css( { + position: 'absolute', + top: 0, + width: contentWrapWidth - ( borderWidth * 2 ) + } ); + } + + $top.css( { + position: 'absolute', + top: heights.menuBarHeight, + width: contentWrapWidth - ( borderWidth * 2 ) - ( visual ? 0 : ( $top.outerWidth() - $top.width() ) ) + } ); + + $statusBar.attr( 'style', advanced ? '' : 'visibility: hidden;' ); + $bottom.attr( 'style', '' ); + } + } else { + // Check if the top is not already in a fixed position. + if ( ( ! fixedTop || resize ) && + ( windowPos >= ( topPos - heights.toolsHeight - heights.adminBarHeight ) && + windowPos <= ( topPos - heights.toolsHeight - heights.adminBarHeight + editorHeight - buffer ) ) ) { + fixedTop = true; + + $tools.css( { + position: 'fixed', + top: heights.adminBarHeight, + width: contentWrapWidth + } ); + + if ( visual && $menuBar.length ) { + $menuBar.css( { + position: 'fixed', + top: heights.adminBarHeight + heights.toolsHeight, + width: contentWrapWidth - ( borderWidth * 2 ) - ( visual ? 0 : ( $top.outerWidth() - $top.width() ) ) + } ); + } + + $top.css( { + position: 'fixed', + top: heights.adminBarHeight + heights.toolsHeight + heights.menuBarHeight, + width: contentWrapWidth - ( borderWidth * 2 ) - ( visual ? 0 : ( $top.outerWidth() - $top.width() ) ) + } ); + // Check if the top is already in a fixed position. + } else if ( fixedTop || resize ) { + if ( windowPos <= ( topPos - heights.toolsHeight - heights.adminBarHeight ) ) { + fixedTop = false; + + $tools.css( { + position: 'absolute', + top: 0, + width: contentWrapWidth + } ); + + if ( visual && $menuBar.length ) { + $menuBar.css( { + position: 'absolute', + top: 0, + width: contentWrapWidth - ( borderWidth * 2 ) + } ); + } + + $top.css( { + position: 'absolute', + top: heights.menuBarHeight, + width: contentWrapWidth - ( borderWidth * 2 ) - ( visual ? 0 : ( $top.outerWidth() - $top.width() ) ) + } ); + } else if ( windowPos >= ( topPos - heights.toolsHeight - heights.adminBarHeight + editorHeight - buffer ) ) { + fixedTop = false; + + $tools.css( { + position: 'absolute', + top: editorHeight - buffer, + width: contentWrapWidth + } ); + + if ( visual && $menuBar.length ) { + $menuBar.css( { + position: 'absolute', + top: editorHeight - buffer, + width: contentWrapWidth - ( borderWidth * 2 ) + } ); + } + + $top.css( { + position: 'absolute', + top: editorHeight - buffer + heights.menuBarHeight, + width: contentWrapWidth - ( borderWidth * 2 ) - ( visual ? 0 : ( $top.outerWidth() - $top.width() ) ) + } ); + } + } + + // Check if the bottom is not already in a fixed position. + if ( ( ! fixedBottom || ( resize && advanced ) ) && + // Add borderWidth for the border around the .wp-editor-container. + ( windowPos + heights.windowHeight ) <= ( editorPos + editorHeight + heights.bottomHeight + heights.statusBarHeight + borderWidth ) ) { + + if ( event && event.deltaHeight > 0 && event.deltaHeight < 100 ) { + window.scrollBy( 0, event.deltaHeight ); + } else if ( visual && advanced ) { + fixedBottom = true; + + $statusBar.css( { + position: 'fixed', + bottom: heights.bottomHeight, + visibility: '', + width: contentWrapWidth - ( borderWidth * 2 ) + } ); + + $bottom.css( { + position: 'fixed', + bottom: 0, + width: contentWrapWidth + } ); + } + } else if ( ( ! advanced && fixedBottom ) || + ( ( fixedBottom || resize ) && + ( windowPos + heights.windowHeight ) > ( editorPos + editorHeight + heights.bottomHeight + heights.statusBarHeight - borderWidth ) ) ) { + fixedBottom = false; + + $statusBar.attr( 'style', advanced ? '' : 'visibility: hidden;' ); + $bottom.attr( 'style', '' ); + } + } + + // The postbox container is positioned with @media from CSS. Ensure it is pinned on the side. + if ( $postboxContainer.width() < 300 && heights.windowWidth > 600 && + + // Check if the sidebar is not taller than the document height. + $document.height() > ( $sideSortables.height() + postBodyTop + 120 ) && + + // Check if the editor is taller than the viewport. + heights.windowHeight < editorHeight ) { + + if ( ( heights.sideSortablesHeight + pinnedToolsTop + sidebarBottom ) > heights.windowHeight || fixedSideTop || fixedSideBottom ) { + + // Reset the sideSortables style when scrolling to the top. + if ( windowPos + pinnedToolsTop <= postBodyTop ) { + $sideSortables.attr( 'style', '' ); + fixedSideTop = fixedSideBottom = false; + } else { + + // When scrolling down. + if ( windowPos > lastScrollPosition ) { + if ( fixedSideTop ) { + + // Let it scroll. + fixedSideTop = false; + sidebarTop = $sideSortables.offset().top - heights.adminBarHeight; + footerTop = $footer.offset().top; + + // Don't get over the footer. + if ( footerTop < sidebarTop + heights.sideSortablesHeight + sidebarBottom ) { + sidebarTop = footerTop - heights.sideSortablesHeight - 12; + } + + $sideSortables.css({ + position: 'absolute', + top: sidebarTop, + bottom: '' + }); + } else if ( ! fixedSideBottom && heights.sideSortablesHeight + $sideSortables.offset().top + sidebarBottom < windowPos + heights.windowHeight ) { + // Pin the bottom. + fixedSideBottom = true; + + $sideSortables.css({ + position: 'fixed', + top: 'auto', + bottom: sidebarBottom + }); + } + + // When scrolling up. + } else if ( windowPos < lastScrollPosition ) { + if ( fixedSideBottom ) { + // Let it scroll. + fixedSideBottom = false; + sidebarTop = $sideSortables.offset().top - sidebarBottom; + footerTop = $footer.offset().top; + + // Don't get over the footer. + if ( footerTop < sidebarTop + heights.sideSortablesHeight + sidebarBottom ) { + sidebarTop = footerTop - heights.sideSortablesHeight - 12; + } + + $sideSortables.css({ + position: 'absolute', + top: sidebarTop, + bottom: '' + }); + } else if ( ! fixedSideTop && $sideSortables.offset().top >= windowPos + pinnedToolsTop ) { + // Pin the top. + fixedSideTop = true; + + $sideSortables.css({ + position: 'fixed', + top: pinnedToolsTop, + bottom: '' + }); + } + } + } + } else { + // If the sidebar container is smaller than the viewport, then pin/unpin the top when scrolling. + if ( windowPos >= ( postBodyTop - pinnedToolsTop ) ) { + + $sideSortables.css( { + position: 'fixed', + top: pinnedToolsTop + } ); + } else { + $sideSortables.attr( 'style', '' ); + } + + fixedSideTop = fixedSideBottom = false; + } + + lastScrollPosition = windowPos; + } else { + $sideSortables.attr( 'style', '' ); + fixedSideTop = fixedSideBottom = false; + } + + if ( resize ) { + $contentWrap.css( { + paddingTop: heights.toolsHeight + } ); + + if ( visual ) { + $visualEditor.css( { + paddingTop: heights.visualTopHeight + heights.menuBarHeight + } ); + } else { + $textEditor.css( { + marginTop: heights.textTopHeight + } ); + } + } + } + + /** + * Resizes the editor and adjusts the toolbars. + * + * @since 4.0.0 + * + * @return {void} + */ + function fullscreenHide() { + textEditorResize(); + adjust(); + } + + /** + * Runs the passed function with 500ms intervals. + * + * @since 4.0.0 + * + * @param {function} callback The function to run in the timeout. + * + * @return {void} + */ + function initialResize( callback ) { + for ( var i = 1; i < 6; i++ ) { + setTimeout( callback, 500 * i ); + } + } + + /** + * Runs adjust after 100ms. + * + * @since 4.0.0 + * + * @return {void} + */ + function afterScroll() { + clearTimeout( scrollTimer ); + scrollTimer = setTimeout( adjust, 100 ); + } + + /** + * Binds editor expand events on elements. + * + * @since 4.0.0 + * + * @return {void} + */ + function on() { + /* + * Scroll to the top when triggering this from JS. + * Ensure the toolbars are pinned properly. + */ + if ( window.pageYOffset && window.pageYOffset > pageYOffsetAtTop ) { + window.scrollTo( window.pageXOffset, 0 ); + } + + $wrap.addClass( 'wp-editor-expand' ); + + // Adjust when the window is scrolled or resized. + $window.on( 'scroll.editor-expand resize.editor-expand', function( event ) { + adjust( event.type ); + afterScroll(); + } ); + + /* + * Adjust when collapsing the menu, changing the columns + * or changing the body class. + */ + $document.on( 'wp-collapse-menu.editor-expand postboxes-columnchange.editor-expand editor-classchange.editor-expand', adjust ) + .on( 'postbox-toggled.editor-expand postbox-moved.editor-expand', function() { + if ( ! fixedSideTop && ! fixedSideBottom && window.pageYOffset > pinnedToolsTop ) { + fixedSideBottom = true; + window.scrollBy( 0, -1 ); + adjust(); + window.scrollBy( 0, 1 ); + } + + adjust(); + }).on( 'wp-window-resized.editor-expand', function() { + if ( mceEditor && ! mceEditor.isHidden() ) { + mceEditor.execCommand( 'wpAutoResize' ); + } else { + textEditorResize(); + } + }); + + $textEditor.on( 'focus.editor-expand input.editor-expand propertychange.editor-expand', textEditorResize ); + mceBind(); + + // Adjust when entering or exiting fullscreen mode. + fullscreen && fullscreen.pubsub.subscribe( 'hidden', fullscreenHide ); + + if ( mceEditor ) { + mceEditor.settings.wp_autoresize_on = true; + mceEditor.execCommand( 'wpAutoResizeOn' ); + + if ( ! mceEditor.isHidden() ) { + mceEditor.execCommand( 'wpAutoResize' ); + } + } + + if ( ! mceEditor || mceEditor.isHidden() ) { + textEditorResize(); + } + + adjust(); + + $document.trigger( 'editor-expand-on' ); + } + + /** + * Unbinds editor expand events. + * + * @since 4.0.0 + * + * @return {void} + */ + function off() { + var height = parseInt( window.getUserSetting( 'ed_size', 300 ), 10 ); + + if ( height < 50 ) { + height = 50; + } else if ( height > 5000 ) { + height = 5000; + } + + /* + * Scroll to the top when triggering this from JS. + * Ensure the toolbars are reset properly. + */ + if ( window.pageYOffset && window.pageYOffset > pageYOffsetAtTop ) { + window.scrollTo( window.pageXOffset, 0 ); + } + + $wrap.removeClass( 'wp-editor-expand' ); + + $window.off( '.editor-expand' ); + $document.off( '.editor-expand' ); + $textEditor.off( '.editor-expand' ); + mceUnbind(); + + // Adjust when entering or exiting fullscreen mode. + fullscreen && fullscreen.pubsub.unsubscribe( 'hidden', fullscreenHide ); + + // Reset all CSS. + $.each( [ $visualTop, $textTop, $tools, $menuBar, $bottom, $statusBar, $contentWrap, $visualEditor, $textEditor, $sideSortables ], function( i, element ) { + element && element.attr( 'style', '' ); + }); + + fixedTop = fixedBottom = fixedSideTop = fixedSideBottom = false; + + if ( mceEditor ) { + mceEditor.settings.wp_autoresize_on = false; + mceEditor.execCommand( 'wpAutoResizeOff' ); + + if ( ! mceEditor.isHidden() ) { + $textEditor.hide(); + + if ( height ) { + mceEditor.theme.resizeTo( null, height ); + } + } + } + + // If there is a height found in the user setting. + if ( height ) { + $textEditor.height( height ); + } + + $document.trigger( 'editor-expand-off' ); + } + + // Start on load. + if ( $wrap.hasClass( 'wp-editor-expand' ) ) { + on(); + + // Resize just after CSS has fully loaded and QuickTags is ready. + if ( $contentWrap.hasClass( 'html-active' ) ) { + initialResize( function() { + adjust(); + textEditorResize(); + } ); + } + } + + // Show the on/off checkbox. + $( '#adv-settings .editor-expand' ).show(); + $( '#editor-expand-toggle' ).on( 'change.editor-expand', function() { + if ( $(this).prop( 'checked' ) ) { + on(); + window.setUserSetting( 'editor_expand', 'on' ); + } else { + off(); + window.setUserSetting( 'editor_expand', 'off' ); + } + }); + + // Expose on() and off(). + window.editorExpand = { + on: on, + off: off + }; + } ); + + /** + * Handles the distraction free writing of TinyMCE. + * + * @since 4.1.0 + * + * @return {void} + */ + $( function() { + var $body = $( document.body ), + $wrap = $( '#wpcontent' ), + $editor = $( '#post-body-content' ), + $title = $( '#title' ), + $content = $( '#content' ), + $overlay = $( document.createElement( 'DIV' ) ), + $slug = $( '#edit-slug-box' ), + $slugFocusEl = $slug.find( 'a' ) + .add( $slug.find( 'button' ) ) + .add( $slug.find( 'input' ) ), + $menuWrap = $( '#adminmenuwrap' ), + $editorWindow = $(), + $editorIframe = $(), + _isActive = window.getUserSetting( 'editor_expand', 'on' ) === 'on', + _isOn = _isActive ? window.getUserSetting( 'post_dfw' ) === 'on' : false, + traveledX = 0, + traveledY = 0, + buffer = 20, + faded, fadedAdminBar, fadedSlug, + editorRect, x, y, mouseY, scrollY, + focusLostTimer, overlayTimer, editorHasFocus; + + $body.append( $overlay ); + + $overlay.css( { + display: 'none', + position: 'fixed', + top: $adminBar.height(), + right: 0, + bottom: 0, + left: 0, + 'z-index': 9997 + } ); + + $editor.css( { + position: 'relative' + } ); + + $window.on( 'mousemove.focus', function( event ) { + mouseY = event.pageY; + } ); + + /** + * Recalculates the bottom and right position of the editor in the DOM. + * + * @since 4.1.0 + * + * @return {void} + */ + function recalcEditorRect() { + editorRect = $editor.offset(); + editorRect.right = editorRect.left + $editor.outerWidth(); + editorRect.bottom = editorRect.top + $editor.outerHeight(); + } + + /** + * Activates the distraction free writing mode. + * + * @since 4.1.0 + * + * @return {void} + */ + function activate() { + if ( ! _isActive ) { + _isActive = true; + + $document.trigger( 'dfw-activate' ); + $content.on( 'keydown.focus-shortcut', toggleViaKeyboard ); + } + } + + /** + * Deactivates the distraction free writing mode. + * + * @since 4.1.0 + * + * @return {void} + */ + function deactivate() { + if ( _isActive ) { + off(); + + _isActive = false; + + $document.trigger( 'dfw-deactivate' ); + $content.off( 'keydown.focus-shortcut' ); + } + } + + /** + * Returns _isActive. + * + * @since 4.1.0 + * + * @return {boolean} Returns true is _isActive is true. + */ + function isActive() { + return _isActive; + } + + /** + * Binds events on the editor for distraction free writing. + * + * @since 4.1.0 + * + * @return {void} + */ + function on() { + if ( ! _isOn && _isActive ) { + _isOn = true; + + $content.on( 'keydown.focus', fadeOut ); + + $title.add( $content ).on( 'blur.focus', maybeFadeIn ); + + fadeOut(); + + window.setUserSetting( 'post_dfw', 'on' ); + + $document.trigger( 'dfw-on' ); + } + } + + /** + * Unbinds events on the editor for distraction free writing. + * + * @since 4.1.0 + * + * @return {void} + */ + function off() { + if ( _isOn ) { + _isOn = false; + + $title.add( $content ).off( '.focus' ); + + fadeIn(); + + $editor.off( '.focus' ); + + window.setUserSetting( 'post_dfw', 'off' ); + + $document.trigger( 'dfw-off' ); + } + } + + /** + * Binds or unbinds the editor expand events. + * + * @since 4.1.0 + * + * @return {void} + */ + function toggle() { + if ( _isOn ) { + off(); + } else { + on(); + } + } + + /** + * Returns the value of _isOn. + * + * @since 4.1.0 + * + * @return {boolean} Returns true if _isOn is true. + */ + function isOn() { + return _isOn; + } + + /** + * Fades out all elements except for the editor. + * + * The fading is done based on key presses and mouse movements. + * Also calls the fadeIn on certain key presses + * or if the mouse leaves the editor. + * + * @since 4.1.0 + * + * @param event The event that triggers this function. + * + * @return {void} + */ + function fadeOut( event ) { + var isMac, + key = event && event.keyCode; + + if ( window.navigator.platform ) { + isMac = ( window.navigator.platform.indexOf( 'Mac' ) > -1 ); + } + + // Fade in and returns on Escape and keyboard shortcut Alt+Shift+W and Ctrl+Opt+W. + if ( key === 27 || ( key === 87 && event.altKey && ( ( ! isMac && event.shiftKey ) || ( isMac && event.ctrlKey ) ) ) ) { + fadeIn( event ); + return; + } + + // Return if any of the following keys or combinations of keys is pressed. + if ( event && ( event.metaKey || ( event.ctrlKey && ! event.altKey ) || ( event.altKey && event.shiftKey ) || ( key && ( + // Special keys ( tab, ctrl, alt, esc, arrow keys... ). + ( key <= 47 && key !== 8 && key !== 13 && key !== 32 && key !== 46 ) || + // Windows keys. + ( key >= 91 && key <= 93 ) || + // F keys. + ( key >= 112 && key <= 135 ) || + // Num Lock, Scroll Lock, OEM. + ( key >= 144 && key <= 150 ) || + // OEM or non-printable. + key >= 224 + ) ) ) ) { + return; + } + + if ( ! faded ) { + faded = true; + + clearTimeout( overlayTimer ); + + overlayTimer = setTimeout( function() { + $overlay.show(); + }, 600 ); + + $editor.css( 'z-index', 9998 ); + + $overlay + // Always recalculate the editor area when entering the overlay with the mouse. + .on( 'mouseenter.focus', function() { + recalcEditorRect(); + + $window.on( 'scroll.focus', function() { + var nScrollY = window.pageYOffset; + + if ( ( + scrollY && mouseY && + scrollY !== nScrollY + ) && ( + mouseY < editorRect.top - buffer || + mouseY > editorRect.bottom + buffer + ) ) { + fadeIn(); + } + + scrollY = nScrollY; + } ); + } ) + .on( 'mouseleave.focus', function() { + x = y = null; + traveledX = traveledY = 0; + + $window.off( 'scroll.focus' ); + } ) + // Fade in when the mouse moves away form the editor area. + .on( 'mousemove.focus', function( event ) { + var nx = event.clientX, + ny = event.clientY, + pageYOffset = window.pageYOffset, + pageXOffset = window.pageXOffset; + + if ( x && y && ( nx !== x || ny !== y ) ) { + if ( + ( ny <= y && ny < editorRect.top - pageYOffset ) || + ( ny >= y && ny > editorRect.bottom - pageYOffset ) || + ( nx <= x && nx < editorRect.left - pageXOffset ) || + ( nx >= x && nx > editorRect.right - pageXOffset ) + ) { + traveledX += Math.abs( x - nx ); + traveledY += Math.abs( y - ny ); + + if ( ( + ny <= editorRect.top - buffer - pageYOffset || + ny >= editorRect.bottom + buffer - pageYOffset || + nx <= editorRect.left - buffer - pageXOffset || + nx >= editorRect.right + buffer - pageXOffset + ) && ( + traveledX > 10 || + traveledY > 10 + ) ) { + fadeIn(); + + x = y = null; + traveledX = traveledY = 0; + + return; + } + } else { + traveledX = traveledY = 0; + } + } + + x = nx; + y = ny; + } ) + + // When the overlay is touched, fade in and cancel the event. + .on( 'touchstart.focus', function( event ) { + event.preventDefault(); + fadeIn(); + } ); + + $editor.off( 'mouseenter.focus' ); + + if ( focusLostTimer ) { + clearTimeout( focusLostTimer ); + focusLostTimer = null; + } + + $body.addClass( 'focus-on' ).removeClass( 'focus-off' ); + } + + fadeOutAdminBar(); + fadeOutSlug(); + } + + /** + * Fades all elements back in. + * + * @since 4.1.0 + * + * @param event The event that triggers this function. + * + * @return {void} + */ + function fadeIn( event ) { + if ( faded ) { + faded = false; + + clearTimeout( overlayTimer ); + + overlayTimer = setTimeout( function() { + $overlay.hide(); + }, 200 ); + + $editor.css( 'z-index', '' ); + + $overlay.off( 'mouseenter.focus mouseleave.focus mousemove.focus touchstart.focus' ); + + /* + * When fading in, temporarily watch for refocus and fade back out - helps + * with 'accidental' editor exits with the mouse. When fading in and the event + * is a key event (Escape or Alt+Shift+W) don't watch for refocus. + */ + if ( 'undefined' === typeof event ) { + $editor.on( 'mouseenter.focus', function() { + if ( $.contains( $editor.get( 0 ), document.activeElement ) || editorHasFocus ) { + fadeOut(); + } + } ); + } + + focusLostTimer = setTimeout( function() { + focusLostTimer = null; + $editor.off( 'mouseenter.focus' ); + }, 1000 ); + + $body.addClass( 'focus-off' ).removeClass( 'focus-on' ); + } + + fadeInAdminBar(); + fadeInSlug(); + } + + /** + * Fades in if the focused element based on it position. + * + * @since 4.1.0 + * + * @return {void} + */ + function maybeFadeIn() { + setTimeout( function() { + var position = document.activeElement.compareDocumentPosition( $editor.get( 0 ) ); + + function hasFocus( $el ) { + return $.contains( $el.get( 0 ), document.activeElement ); + } + + // The focused node is before or behind the editor area, and not outside the wrap. + if ( ( position === 2 || position === 4 ) && ( hasFocus( $menuWrap ) || hasFocus( $wrap ) || hasFocus( $footer ) ) ) { + fadeIn(); + } + }, 0 ); + } + + /** + * Fades out the admin bar based on focus on the admin bar. + * + * @since 4.1.0 + * + * @return {void} + */ + function fadeOutAdminBar() { + if ( ! fadedAdminBar && faded ) { + fadedAdminBar = true; + + $adminBar + .on( 'mouseenter.focus', function() { + $adminBar.addClass( 'focus-off' ); + } ) + .on( 'mouseleave.focus', function() { + $adminBar.removeClass( 'focus-off' ); + } ); + } + } + + /** + * Fades in the admin bar. + * + * @since 4.1.0 + * + * @return {void} + */ + function fadeInAdminBar() { + if ( fadedAdminBar ) { + fadedAdminBar = false; + + $adminBar.off( '.focus' ); + } + } + + /** + * Fades out the edit slug box. + * + * @since 4.1.0 + * + * @return {void} + */ + function fadeOutSlug() { + if ( ! fadedSlug && faded && ! $slug.find( ':focus').length ) { + fadedSlug = true; + + $slug.stop().fadeTo( 'fast', 0.3 ).on( 'mouseenter.focus', fadeInSlug ).off( 'mouseleave.focus' ); + + $slugFocusEl.on( 'focus.focus', fadeInSlug ).off( 'blur.focus' ); + } + } + + /** + * Fades in the edit slug box. + * + * @since 4.1.0 + * + * @return {void} + */ + function fadeInSlug() { + if ( fadedSlug ) { + fadedSlug = false; + + $slug.stop().fadeTo( 'fast', 1 ).on( 'mouseleave.focus', fadeOutSlug ).off( 'mouseenter.focus' ); + + $slugFocusEl.on( 'blur.focus', fadeOutSlug ).off( 'focus.focus' ); + } + } + + /** + * Triggers the toggle on Alt + Shift + W. + * + * Keycode 87 = w. + * + * @since 4.1.0 + * + * @param {event} event The event to trigger the toggle. + * + * @return {void} + */ + function toggleViaKeyboard( event ) { + if ( event.altKey && event.shiftKey && 87 === event.keyCode ) { + toggle(); + } + } + + if ( $( '#postdivrich' ).hasClass( 'wp-editor-expand' ) ) { + $content.on( 'keydown.focus-shortcut', toggleViaKeyboard ); + } + + /** + * Adds the distraction free writing button when setting up TinyMCE. + * + * @since 4.1.0 + * + * @param {event} event The TinyMCE editor setup event. + * @param {object} editor The editor to add the button to. + * + * @return {void} + */ + $document.on( 'tinymce-editor-setup.focus', function( event, editor ) { + editor.addButton( 'dfw', { + active: _isOn, + classes: 'wp-dfw btn widget', + disabled: ! _isActive, + onclick: toggle, + onPostRender: function() { + var button = this; + + editor.on( 'init', function() { + if ( button.disabled() ) { + button.hide(); + } + } ); + + $document + .on( 'dfw-activate.focus', function() { + button.disabled( false ); + button.show(); + } ) + .on( 'dfw-deactivate.focus', function() { + button.disabled( true ); + button.hide(); + } ) + .on( 'dfw-on.focus', function() { + button.active( true ); + } ) + .on( 'dfw-off.focus', function() { + button.active( false ); + } ); + }, + tooltip: 'Distraction-free writing mode', + shortcut: 'Alt+Shift+W' + } ); + + editor.addCommand( 'wpToggleDFW', toggle ); + editor.addShortcut( 'access+w', '', 'wpToggleDFW' ); + } ); + + /** + * Binds and unbinds events on the editor. + * + * @since 4.1.0 + * + * @param {event} event The TinyMCE editor init event. + * @param {object} editor The editor to bind events on. + * + * @return {void} + */ + $document.on( 'tinymce-editor-init.focus', function( event, editor ) { + var mceBind, mceUnbind; + + function focus() { + editorHasFocus = true; + } + + function blur() { + editorHasFocus = false; + } + + if ( editor.id === 'content' ) { + $editorWindow = $( editor.getWin() ); + $editorIframe = $( editor.getContentAreaContainer() ).find( 'iframe' ); + + mceBind = function() { + editor.on( 'keydown', fadeOut ); + editor.on( 'blur', maybeFadeIn ); + editor.on( 'focus', focus ); + editor.on( 'blur', blur ); + editor.on( 'wp-autoresize', recalcEditorRect ); + }; + + mceUnbind = function() { + editor.off( 'keydown', fadeOut ); + editor.off( 'blur', maybeFadeIn ); + editor.off( 'focus', focus ); + editor.off( 'blur', blur ); + editor.off( 'wp-autoresize', recalcEditorRect ); + }; + + if ( _isOn ) { + mceBind(); + } + + // Bind and unbind based on the distraction free writing focus. + $document.on( 'dfw-on.focus', mceBind ).on( 'dfw-off.focus', mceUnbind ); + + // Focuse the editor when it is the target of the click event. + editor.on( 'click', function( event ) { + if ( event.target === editor.getDoc().documentElement ) { + editor.focus(); + } + } ); + } + } ); + + /** + * Binds events on quicktags init. + * + * @since 4.1.0 + * + * @param {event} event The quicktags init event. + * @param {object} editor The editor to bind events on. + * + * @return {void} + */ + $document.on( 'quicktags-init', function( event, editor ) { + var $button; + + // Bind the distraction free writing events if the distraction free writing button is available. + if ( editor.settings.buttons && ( ',' + editor.settings.buttons + ',' ).indexOf( ',dfw,' ) !== -1 ) { + $button = $( '#' + editor.name + '_dfw' ); + + $( document ) + .on( 'dfw-activate', function() { + $button.prop( 'disabled', false ); + } ) + .on( 'dfw-deactivate', function() { + $button.prop( 'disabled', true ); + } ) + .on( 'dfw-on', function() { + $button.addClass( 'active' ); + } ) + .on( 'dfw-off', function() { + $button.removeClass( 'active' ); + } ); + } + } ); + + $document.on( 'editor-expand-on.focus', activate ).on( 'editor-expand-off.focus', deactivate ); + + if ( _isOn ) { + $content.on( 'keydown.focus', fadeOut ); + + $title.add( $content ).on( 'blur.focus', maybeFadeIn ); + } + + window.wp = window.wp || {}; + window.wp.editor = window.wp.editor || {}; + window.wp.editor.dfw = { + activate: activate, + deactivate: deactivate, + isActive: isActive, + on: on, + off: off, + toggle: toggle, + isOn: isOn + }; + } ); +} )( window, window.jQuery ); diff --git a/wp-admin/js/editor-expand.min.js b/wp-admin/js/editor-expand.min.js new file mode 100644 index 0000000..1432a36 --- /dev/null +++ b/wp-admin/js/editor-expand.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(F,I){"use strict";var L=I(F),M=I(document),V=I("#wpadminbar"),N=I("#wpfooter");I(function(){var g,e,u=I("#postdivrich"),h=I("#wp-content-wrap"),m=I("#wp-content-editor-tools"),w=I(),H=I(),b=I("#ed_toolbar"),v=I("#content"),i=v[0],o=0,x=I("#post-status-info"),y=I(),T=I(),B=I("#side-sortables"),C=I("#postbox-container-1"),S=I("#post-body"),O=F.wp.editor&&F.wp.editor.fullscreen,r=function(){},l=function(){},z=!1,E=!1,k=!1,A=!1,W=0,K=56,R=20,Y=300,f=h.hasClass("tmce-active")?"tinymce":"html",U=!!parseInt(F.getUserSetting("hidetb"),10),D={windowHeight:0,windowWidth:0,adminBarHeight:0,toolsHeight:0,menuBarHeight:0,visualTopHeight:0,textTopHeight:0,bottomHeight:0,statusBarHeight:0,sideSortablesHeight:0},a=F._.throttle(function(){var t=F.scrollX||document.documentElement.scrollLeft,e=F.scrollY||document.documentElement.scrollTop,o=parseInt(i.style.height,10);i.style.height=Y+"px",i.scrollHeight>Y&&(i.style.height=i.scrollHeight+"px"),void 0!==t&&F.scrollTo(t,e),i.scrollHeight<o&&p()},300);function P(){var t=i.value.length;g&&!g.isHidden()||!g&&"tinymce"===f||(t<o?a():parseInt(i.style.height,10)<i.scrollHeight&&(i.style.height=Math.ceil(i.scrollHeight)+"px",p()),o=t)}function p(t){var e,o,i,n,s,f,a,d,c,u,r,l,p;O&&O.settings.visible||(e=L.scrollTop(),o="scroll"!==(u=t&&t.type),i=g&&!g.isHidden(),n=Y,s=S.offset().top,f=h.width(),!o&&D.windowHeight||(p=L.width(),(D={windowHeight:L.height(),windowWidth:p,adminBarHeight:600<p?V.outerHeight():0,toolsHeight:m.outerHeight()||0,menuBarHeight:y.outerHeight()||0,visualTopHeight:w.outerHeight()||0,textTopHeight:b.outerHeight()||0,bottomHeight:x.outerHeight()||0,statusBarHeight:T.outerHeight()||0,sideSortablesHeight:B.height()||0}).menuBarHeight<3&&(D.menuBarHeight=0)),i||"resize"!==u||P(),p=i?(a=w,l=H,D.visualTopHeight):(a=b,l=v,D.textTopHeight),(i||a.length)&&(u=a.parent().offset().top,r=l.offset().top,l=l.outerHeight(),(i?Y+p:Y+20)+5<l?((!z||o)&&e>=u-D.toolsHeight-D.adminBarHeight&&e<=u-D.toolsHeight-D.adminBarHeight+l-n?(z=!0,m.css({position:"fixed",top:D.adminBarHeight,width:f}),i&&y.length&&y.css({position:"fixed",top:D.adminBarHeight+D.toolsHeight,width:f-2-(i?0:a.outerWidth()-a.width())}),a.css({position:"fixed",top:D.adminBarHeight+D.toolsHeight+D.menuBarHeight,width:f-2-(i?0:a.outerWidth()-a.width())})):(z||o)&&(e<=u-D.toolsHeight-D.adminBarHeight?(z=!1,m.css({position:"absolute",top:0,width:f}),i&&y.length&&y.css({position:"absolute",top:0,width:f-2}),a.css({position:"absolute",top:D.menuBarHeight,width:f-2-(i?0:a.outerWidth()-a.width())})):e>=u-D.toolsHeight-D.adminBarHeight+l-n&&(z=!1,m.css({position:"absolute",top:l-n,width:f}),i&&y.length&&y.css({position:"absolute",top:l-n,width:f-2}),a.css({position:"absolute",top:l-n+D.menuBarHeight,width:f-2-(i?0:a.outerWidth()-a.width())}))),(!E||o&&U)&&e+D.windowHeight<=r+l+D.bottomHeight+D.statusBarHeight+1?t&&0<t.deltaHeight&&t.deltaHeight<100?F.scrollBy(0,t.deltaHeight):i&&U&&(E=!0,T.css({position:"fixed",bottom:D.bottomHeight,visibility:"",width:f-2}),x.css({position:"fixed",bottom:0,width:f})):(!U&&E||(E||o)&&e+D.windowHeight>r+l+D.bottomHeight+D.statusBarHeight-1)&&(E=!1,T.attr("style",U?"":"visibility: hidden;"),x.attr("style",""))):o&&(m.css({position:"absolute",top:0,width:f}),i&&y.length&&y.css({position:"absolute",top:0,width:f-2}),a.css({position:"absolute",top:D.menuBarHeight,width:f-2-(i?0:a.outerWidth()-a.width())}),T.attr("style",U?"":"visibility: hidden;"),x.attr("style","")),C.width()<300&&600<D.windowWidth&&M.height()>B.height()+s+120&&D.windowHeight<l?(D.sideSortablesHeight+K+R>D.windowHeight||k||A?e+K<=s?(B.attr("style",""),k=A=!1):W<e?k?(k=!1,d=B.offset().top-D.adminBarHeight,(c=N.offset().top)<d+D.sideSortablesHeight+R&&(d=c-D.sideSortablesHeight-12),B.css({position:"absolute",top:d,bottom:""})):!A&&D.sideSortablesHeight+B.offset().top+R<e+D.windowHeight&&(A=!0,B.css({position:"fixed",top:"auto",bottom:R})):e<W&&(A?(A=!1,d=B.offset().top-R,(c=N.offset().top)<d+D.sideSortablesHeight+R&&(d=c-D.sideSortablesHeight-12),B.css({position:"absolute",top:d,bottom:""})):!k&&B.offset().top>=e+K&&(k=!0,B.css({position:"fixed",top:K,bottom:""}))):(s-K<=e?B.css({position:"fixed",top:K}):B.attr("style",""),k=A=!1),W=e):(B.attr("style",""),k=A=!1),o)&&(h.css({paddingTop:D.toolsHeight}),i?H.css({paddingTop:D.visualTopHeight+D.menuBarHeight}):v.css({marginTop:D.textTopHeight})))}function n(){P(),p()}function X(t){for(var e=1;e<6;e++)setTimeout(t,500*e)}function t(){F.pageYOffset&&130<F.pageYOffset&&F.scrollTo(F.pageXOffset,0),u.addClass("wp-editor-expand"),L.on("scroll.editor-expand resize.editor-expand",function(t){p(t.type),clearTimeout(e),e=setTimeout(p,100)}),M.on("wp-collapse-menu.editor-expand postboxes-columnchange.editor-expand editor-classchange.editor-expand",p).on("postbox-toggled.editor-expand postbox-moved.editor-expand",function(){!k&&!A&&F.pageYOffset>K&&(A=!0,F.scrollBy(0,-1),p(),F.scrollBy(0,1)),p()}).on("wp-window-resized.editor-expand",function(){g&&!g.isHidden()?g.execCommand("wpAutoResize"):P()}),v.on("focus.editor-expand input.editor-expand propertychange.editor-expand",P),r(),O&&O.pubsub.subscribe("hidden",n),g&&(g.settings.wp_autoresize_on=!0,g.execCommand("wpAutoResizeOn"),g.isHidden()||g.execCommand("wpAutoResize")),g&&!g.isHidden()||P(),p(),M.trigger("editor-expand-on")}function s(){var t=parseInt(F.getUserSetting("ed_size",300),10);t<50?t=50:5e3<t&&(t=5e3),F.pageYOffset&&130<F.pageYOffset&&F.scrollTo(F.pageXOffset,0),u.removeClass("wp-editor-expand"),L.off(".editor-expand"),M.off(".editor-expand"),v.off(".editor-expand"),l(),O&&O.pubsub.unsubscribe("hidden",n),I.each([w,b,m,y,x,T,h,H,v,B],function(t,e){e&&e.attr("style","")}),z=E=k=A=!1,g&&(g.settings.wp_autoresize_on=!1,g.execCommand("wpAutoResizeOff"),g.isHidden()||(v.hide(),t&&g.theme.resizeTo(null,t))),t&&v.height(t),M.trigger("editor-expand-off")}M.on("tinymce-editor-init.editor-expand",function(t,f){var a=F.tinymce.util.VK,e=_.debounce(function(){I(".mce-floatpanel:hover").length||F.tinymce.ui.FloatPanel.hideAll(),I(".mce-tooltip").hide()},1e3,!0);function o(t){t=t.keyCode;t<=47&&t!==a.SPACEBAR&&t!==a.ENTER&&t!==a.DELETE&&t!==a.BACKSPACE&&t!==a.UP&&t!==a.LEFT&&t!==a.DOWN&&t!==a.UP||91<=t&&t<=93||112<=t&&t<=123||144===t||145===t||i(t)}function i(t){var e,o,i,n,s=function(){var t,e,o=f.selection.getNode();if(f.wp&&f.wp.getView&&(t=f.wp.getView(o)))e=t.getBoundingClientRect();else{t=f.selection.getRng();try{e=t.getClientRects()[0]}catch(t){}e=e||o.getBoundingClientRect()}return!!e.height&&e}();s&&(o=(e=s.top+f.iframeElement.getBoundingClientRect().top)+s.height,e-=50,o+=50,i=D.adminBarHeight+D.toolsHeight+D.menuBarHeight+D.visualTopHeight,(n=D.windowHeight-(U?D.bottomHeight+D.statusBarHeight:0))-i<s.height||(e<i&&(t===a.UP||t===a.LEFT||t===a.BACKSPACE)?F.scrollTo(F.pageXOffset,e+F.pageYOffset-i):n<o&&F.scrollTo(F.pageXOffset,o+F.pageYOffset-n)))}function n(t){t.state||p()}function s(){L.on("scroll.mce-float-panels",e),setTimeout(function(){f.execCommand("wpAutoResize"),p()},300)}function d(){L.off("scroll.mce-float-panels"),setTimeout(function(){var t=h.offset().top;F.pageYOffset>t&&F.scrollTo(F.pageXOffset,t-D.adminBarHeight),P(),p()},100),p()}function c(){U=!U}"content"===f.id&&((g=f).settings.autoresize_min_height=Y,w=h.find(".mce-toolbar-grp"),H=h.find(".mce-edit-area"),T=h.find(".mce-statusbar"),y=h.find(".mce-menubar"),r=function(){f.on("keyup",o),f.on("show",s),f.on("hide",d),f.on("wp-toolbar-toggle",c),f.on("setcontent wp-autoresize wp-toolbar-toggle",p),f.on("undo redo",i),f.on("FullscreenStateChanged",n),L.off("scroll.mce-float-panels").on("scroll.mce-float-panels",e)},l=function(){f.off("keyup",o),f.off("show",s),f.off("hide",d),f.off("wp-toolbar-toggle",c),f.off("setcontent wp-autoresize wp-toolbar-toggle",p),f.off("undo redo",i),f.off("FullscreenStateChanged",n),L.off("scroll.mce-float-panels")},u.hasClass("wp-editor-expand"))&&(r(),X(p))}),u.hasClass("wp-editor-expand")&&(t(),h.hasClass("html-active"))&&X(function(){p(),P()}),I("#adv-settings .editor-expand").show(),I("#editor-expand-toggle").on("change.editor-expand",function(){I(this).prop("checked")?(t(),F.setUserSetting("editor_expand","on")):(s(),F.setUserSetting("editor_expand","off"))}),F.editorExpand={on:t,off:s}}),I(function(){var i,n,t,s,f,a,d,c,u,r,l,p=I(document.body),o=I("#wpcontent"),g=I("#post-body-content"),e=I("#title"),h=I("#content"),m=I(document.createElement("DIV")),w=I("#edit-slug-box"),H=w.find("a").add(w.find("button")).add(w.find("input")),Y=I("#adminmenuwrap"),b=(I(),I(),"on"===F.getUserSetting("editor_expand","on")),v=!!b&&"on"===F.getUserSetting("post_dfw"),x=0,y=0,T=20;function B(){(s=g.offset()).right=s.left+g.outerWidth(),s.bottom=s.top+g.outerHeight()}function C(){b||(b=!0,M.trigger("dfw-activate"),h.on("keydown.focus-shortcut",R))}function S(){b&&(z(),b=!1,M.trigger("dfw-deactivate"),h.off("keydown.focus-shortcut"))}function O(){!v&&b&&(v=!0,h.on("keydown.focus",_),e.add(h).on("blur.focus",A),_(),F.setUserSetting("post_dfw","on"),M.trigger("dfw-on"))}function z(){v&&(v=!1,e.add(h).off(".focus"),k(),g.off(".focus"),F.setUserSetting("post_dfw","off"),M.trigger("dfw-off"))}function E(){(v?z:O)()}function _(t){var e,o=t&&t.keyCode;F.navigator.platform&&(e=-1<F.navigator.platform.indexOf("Mac")),27===o||87===o&&t.altKey&&(!e&&t.shiftKey||e&&t.ctrlKey)?k(t):t&&(t.metaKey||t.ctrlKey&&!t.altKey||t.altKey&&t.shiftKey||o&&(o<=47&&8!==o&&13!==o&&32!==o&&46!==o||91<=o&&o<=93||112<=o&&o<=135||144<=o&&o<=150||224<=o))||(i||(i=!0,clearTimeout(r),r=setTimeout(function(){m.show()},600),g.css("z-index",9998),m.on("mouseenter.focus",function(){B(),L.on("scroll.focus",function(){var t=F.pageYOffset;c&&d&&c!==t&&(d<s.top-T||d>s.bottom+T)&&k(),c=t})}).on("mouseleave.focus",function(){f=a=null,x=y=0,L.off("scroll.focus")}).on("mousemove.focus",function(t){var e=t.clientX,t=t.clientY,o=F.pageYOffset,i=F.pageXOffset;if(f&&a&&(e!==f||t!==a))if(t<=a&&t<s.top-o||a<=t&&t>s.bottom-o||e<=f&&e<s.left-i||f<=e&&e>s.right-i){if(x+=Math.abs(f-e),y+=Math.abs(a-t),(t<=s.top-T-o||t>=s.bottom+T-o||e<=s.left-T-i||e>=s.right+T-i)&&(10<x||10<y))return k(),f=a=null,void(x=y=0)}else x=y=0;f=e,a=t}).on("touchstart.focus",function(t){t.preventDefault(),k()}),g.off("mouseenter.focus"),u&&(clearTimeout(u),u=null),p.addClass("focus-on").removeClass("focus-off")),!n&&i&&(n=!0,V.on("mouseenter.focus",function(){V.addClass("focus-off")}).on("mouseleave.focus",function(){V.removeClass("focus-off")})),W())}function k(t){i&&(i=!1,clearTimeout(r),r=setTimeout(function(){m.hide()},200),g.css("z-index",""),m.off("mouseenter.focus mouseleave.focus mousemove.focus touchstart.focus"),void 0===t&&g.on("mouseenter.focus",function(){(I.contains(g.get(0),document.activeElement)||l)&&_()}),u=setTimeout(function(){u=null,g.off("mouseenter.focus")},1e3),p.addClass("focus-off").removeClass("focus-on")),n&&(n=!1,V.off(".focus")),K()}function A(){setTimeout(function(){var t=document.activeElement.compareDocumentPosition(g.get(0));function e(t){return I.contains(t.get(0),document.activeElement)}2!==t&&4!==t||!(e(Y)||e(o)||e(N))||k()},0)}function W(){t||!i||w.find(":focus").length||(t=!0,w.stop().fadeTo("fast",.3).on("mouseenter.focus",K).off("mouseleave.focus"),H.on("focus.focus",K).off("blur.focus"))}function K(){t&&(t=!1,w.stop().fadeTo("fast",1).on("mouseleave.focus",W).off("mouseenter.focus"),H.on("blur.focus",W).off("focus.focus"))}function R(t){t.altKey&&t.shiftKey&&87===t.keyCode&&E()}p.append(m),m.css({display:"none",position:"fixed",top:V.height(),right:0,bottom:0,left:0,"z-index":9997}),g.css({position:"relative"}),L.on("mousemove.focus",function(t){d=t.pageY}),I("#postdivrich").hasClass("wp-editor-expand")&&h.on("keydown.focus-shortcut",R),M.on("tinymce-editor-setup.focus",function(t,e){e.addButton("dfw",{active:v,classes:"wp-dfw btn widget",disabled:!b,onclick:E,onPostRender:function(){var t=this;e.on("init",function(){t.disabled()&&t.hide()}),M.on("dfw-activate.focus",function(){t.disabled(!1),t.show()}).on("dfw-deactivate.focus",function(){t.disabled(!0),t.hide()}).on("dfw-on.focus",function(){t.active(!0)}).on("dfw-off.focus",function(){t.active(!1)})},tooltip:"Distraction-free writing mode",shortcut:"Alt+Shift+W"}),e.addCommand("wpToggleDFW",E),e.addShortcut("access+w","","wpToggleDFW")}),M.on("tinymce-editor-init.focus",function(t,e){var o,i;function n(){l=!0}function s(){l=!1}"content"===e.id&&(I(e.getWin()),I(e.getContentAreaContainer()).find("iframe"),o=function(){e.on("keydown",_),e.on("blur",A),e.on("focus",n),e.on("blur",s),e.on("wp-autoresize",B)},i=function(){e.off("keydown",_),e.off("blur",A),e.off("focus",n),e.off("blur",s),e.off("wp-autoresize",B)},v&&o(),M.on("dfw-on.focus",o).on("dfw-off.focus",i),e.on("click",function(t){t.target===e.getDoc().documentElement&&e.focus()}))}),M.on("quicktags-init",function(t,e){var o;e.settings.buttons&&-1!==(","+e.settings.buttons+",").indexOf(",dfw,")&&(o=I("#"+e.name+"_dfw"),I(document).on("dfw-activate",function(){o.prop("disabled",!1)}).on("dfw-deactivate",function(){o.prop("disabled",!0)}).on("dfw-on",function(){o.addClass("active")}).on("dfw-off",function(){o.removeClass("active")}))}),M.on("editor-expand-on.focus",C).on("editor-expand-off.focus",S),v&&(h.on("keydown.focus",_),e.add(h).on("blur.focus",A)),F.wp=F.wp||{},F.wp.editor=F.wp.editor||{},F.wp.editor.dfw={activate:C,deactivate:S,isActive:function(){return b},on:O,off:z,toggle:E,isOn:function(){return v}}})}(window,window.jQuery);
\ No newline at end of file diff --git a/wp-admin/js/editor.js b/wp-admin/js/editor.js new file mode 100644 index 0000000..d5fe958 --- /dev/null +++ b/wp-admin/js/editor.js @@ -0,0 +1,1416 @@ +/** + * @output wp-admin/js/editor.js + */ + +window.wp = window.wp || {}; + +( function( $, wp ) { + wp.editor = wp.editor || {}; + + /** + * Utility functions for the editor. + * + * @since 2.5.0 + */ + function SwitchEditors() { + var tinymce, $$, + exports = {}; + + function init() { + if ( ! tinymce && window.tinymce ) { + tinymce = window.tinymce; + $$ = tinymce.$; + + /** + * Handles onclick events for the Visual/Text tabs. + * + * @since 4.3.0 + * + * @return {void} + */ + $$( document ).on( 'click', function( event ) { + var id, mode, + target = $$( event.target ); + + if ( target.hasClass( 'wp-switch-editor' ) ) { + id = target.attr( 'data-wp-editor-id' ); + mode = target.hasClass( 'switch-tmce' ) ? 'tmce' : 'html'; + switchEditor( id, mode ); + } + }); + } + } + + /** + * Returns the height of the editor toolbar(s) in px. + * + * @since 3.9.0 + * + * @param {Object} editor The TinyMCE editor. + * @return {number} If the height is between 10 and 200 return the height, + * else return 30. + */ + function getToolbarHeight( editor ) { + var node = $$( '.mce-toolbar-grp', editor.getContainer() )[0], + height = node && node.clientHeight; + + if ( height && height > 10 && height < 200 ) { + return parseInt( height, 10 ); + } + + return 30; + } + + /** + * Switches the editor between Visual and Text mode. + * + * @since 2.5.0 + * + * @memberof switchEditors + * + * @param {string} id The id of the editor you want to change the editor mode for. Default: `content`. + * @param {string} mode The mode you want to switch to. Default: `toggle`. + * @return {void} + */ + function switchEditor( id, mode ) { + id = id || 'content'; + mode = mode || 'toggle'; + + var editorHeight, toolbarHeight, iframe, + editor = tinymce.get( id ), + wrap = $$( '#wp-' + id + '-wrap' ), + $textarea = $$( '#' + id ), + textarea = $textarea[0]; + + if ( 'toggle' === mode ) { + if ( editor && ! editor.isHidden() ) { + mode = 'html'; + } else { + mode = 'tmce'; + } + } + + if ( 'tmce' === mode || 'tinymce' === mode ) { + // If the editor is visible we are already in `tinymce` mode. + if ( editor && ! editor.isHidden() ) { + return false; + } + + // Insert closing tags for any open tags in QuickTags. + if ( typeof( window.QTags ) !== 'undefined' ) { + window.QTags.closeAllTags( id ); + } + + editorHeight = parseInt( textarea.style.height, 10 ) || 0; + + var keepSelection = false; + if ( editor ) { + keepSelection = editor.getParam( 'wp_keep_scroll_position' ); + } else { + keepSelection = window.tinyMCEPreInit.mceInit[ id ] && + window.tinyMCEPreInit.mceInit[ id ].wp_keep_scroll_position; + } + + if ( keepSelection ) { + // Save the selection. + addHTMLBookmarkInTextAreaContent( $textarea ); + } + + if ( editor ) { + editor.show(); + + // No point to resize the iframe in iOS. + if ( ! tinymce.Env.iOS && editorHeight ) { + toolbarHeight = getToolbarHeight( editor ); + editorHeight = editorHeight - toolbarHeight + 14; + + // Sane limit for the editor height. + if ( editorHeight > 50 && editorHeight < 5000 ) { + editor.theme.resizeTo( null, editorHeight ); + } + } + + if ( editor.getParam( 'wp_keep_scroll_position' ) ) { + // Restore the selection. + focusHTMLBookmarkInVisualEditor( editor ); + } + } else { + tinymce.init( window.tinyMCEPreInit.mceInit[ id ] ); + } + + wrap.removeClass( 'html-active' ).addClass( 'tmce-active' ); + $textarea.attr( 'aria-hidden', true ); + window.setUserSetting( 'editor', 'tinymce' ); + + } else if ( 'html' === mode ) { + // If the editor is hidden (Quicktags is shown) we don't need to switch. + if ( editor && editor.isHidden() ) { + return false; + } + + if ( editor ) { + // Don't resize the textarea in iOS. + // The iframe is forced to 100% height there, we shouldn't match it. + if ( ! tinymce.Env.iOS ) { + iframe = editor.iframeElement; + editorHeight = iframe ? parseInt( iframe.style.height, 10 ) : 0; + + if ( editorHeight ) { + toolbarHeight = getToolbarHeight( editor ); + editorHeight = editorHeight + toolbarHeight - 14; + + // Sane limit for the textarea height. + if ( editorHeight > 50 && editorHeight < 5000 ) { + textarea.style.height = editorHeight + 'px'; + } + } + } + + var selectionRange = null; + + if ( editor.getParam( 'wp_keep_scroll_position' ) ) { + selectionRange = findBookmarkedPosition( editor ); + } + + editor.hide(); + + if ( selectionRange ) { + selectTextInTextArea( editor, selectionRange ); + } + } else { + // There is probably a JS error on the page. + // The TinyMCE editor instance doesn't exist. Show the textarea. + $textarea.css({ 'display': '', 'visibility': '' }); + } + + wrap.removeClass( 'tmce-active' ).addClass( 'html-active' ); + $textarea.attr( 'aria-hidden', false ); + window.setUserSetting( 'editor', 'html' ); + } + } + + /** + * Checks if a cursor is inside an HTML tag or comment. + * + * In order to prevent breaking HTML tags when selecting text, the cursor + * must be moved to either the start or end of the tag. + * + * This will prevent the selection marker to be inserted in the middle of an HTML tag. + * + * This function gives information whether the cursor is inside a tag or not, as well as + * the tag type, if it is a closing tag and check if the HTML tag is inside a shortcode tag, + * e.g. `[caption]<img.../>..`. + * + * @param {string} content The test content where the cursor is. + * @param {number} cursorPosition The cursor position inside the content. + * + * @return {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag. + */ + function getContainingTagInfo( content, cursorPosition ) { + var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ), + lastGtPos = content.lastIndexOf( '>', cursorPosition ); + + if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) { + // Find what the tag is. + var tagContent = content.substr( lastLtPos ), + tagMatch = tagContent.match( /<\s*(\/)?(\w+|\!-{2}.*-{2})/ ); + + if ( ! tagMatch ) { + return null; + } + + var tagType = tagMatch[2], + closingGt = tagContent.indexOf( '>' ); + + return { + ltPos: lastLtPos, + gtPos: lastLtPos + closingGt + 1, // Offset by one to get the position _after_ the character. + tagType: tagType, + isClosingTag: !! tagMatch[1] + }; + } + return null; + } + + /** + * Checks if the cursor is inside a shortcode + * + * If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to + * move the selection marker to before or after the shortcode. + * + * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the + * `<img/>` tag inside. + * + * `[caption]<span>ThisIsGone</span><img .../>[caption]` + * + * Moving the selection to before or after the short code is better, since it allows to select + * something, instead of just losing focus and going to the start of the content. + * + * @param {string} content The text content to check against. + * @param {number} cursorPosition The cursor position to check. + * + * @return {(undefined|Object)} Undefined if the cursor is not wrapped in a shortcode tag. + * Information about the wrapping shortcode tag if it's wrapped in one. + */ + function getShortcodeWrapperInfo( content, cursorPosition ) { + var contentShortcodes = getShortCodePositionsInText( content ); + + for ( var i = 0; i < contentShortcodes.length; i++ ) { + var element = contentShortcodes[ i ]; + + if ( cursorPosition >= element.startIndex && cursorPosition <= element.endIndex ) { + return element; + } + } + } + + /** + * Gets a list of unique shortcodes or shortcode-look-alikes in the content. + * + * @param {string} content The content we want to scan for shortcodes. + */ + function getShortcodesInText( content ) { + var shortcodes = content.match( /\[+([\w_-])+/g ), + result = []; + + if ( shortcodes ) { + for ( var i = 0; i < shortcodes.length; i++ ) { + var shortcode = shortcodes[ i ].replace( /^\[+/g, '' ); + + if ( result.indexOf( shortcode ) === -1 ) { + result.push( shortcode ); + } + } + } + + return result; + } + + /** + * Gets all shortcodes and their positions in the content + * + * This function returns all the shortcodes that could be found in the textarea content + * along with their character positions and boundaries. + * + * This is used to check if the selection cursor is inside the boundaries of a shortcode + * and move it accordingly, to avoid breakage. + * + * @link adjustTextAreaSelectionCursors + * + * The information can also be used in other cases when we need to lookup shortcode data, + * as it's already structured! + * + * @param {string} content The content we want to scan for shortcodes + */ + function getShortCodePositionsInText( content ) { + var allShortcodes = getShortcodesInText( content ), shortcodeInfo; + + if ( allShortcodes.length === 0 ) { + return []; + } + + var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ), + shortcodeMatch, // Define local scope for the variable to be used in the loop below. + shortcodesDetails = []; + + while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) { + /** + * Check if the shortcode should be shown as plain text. + * + * This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode + * and just shows it as text. + */ + var showAsPlainText = shortcodeMatch[1] === '['; + + shortcodeInfo = { + shortcodeName: shortcodeMatch[2], + showAsPlainText: showAsPlainText, + startIndex: shortcodeMatch.index, + endIndex: shortcodeMatch.index + shortcodeMatch[0].length, + length: shortcodeMatch[0].length + }; + + shortcodesDetails.push( shortcodeInfo ); + } + + /** + * Get all URL matches, and treat them as embeds. + * + * Since there isn't a good way to detect if a URL by itself on a line is a previewable + * object, it's best to treat all of them as such. + * + * This means that the selection will capture the whole URL, in a similar way shrotcodes + * are treated. + */ + var urlRegexp = new RegExp( + '(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi' + ); + + while ( shortcodeMatch = urlRegexp.exec( content ) ) { + shortcodeInfo = { + shortcodeName: 'url', + showAsPlainText: false, + startIndex: shortcodeMatch.index, + endIndex: shortcodeMatch.index + shortcodeMatch[ 0 ].length, + length: shortcodeMatch[ 0 ].length, + urlAtStartOfContent: shortcodeMatch[ 1 ] === '', + urlAtEndOfContent: shortcodeMatch[ 3 ] === '' + }; + + shortcodesDetails.push( shortcodeInfo ); + } + + return shortcodesDetails; + } + + /** + * Generate a cursor marker element to be inserted in the content. + * + * `span` seems to be the least destructive element that can be used. + * + * Using DomQuery syntax to create it, since it's used as both text and as a DOM element. + * + * @param {Object} domLib DOM library instance. + * @param {string} content The content to insert into the cursor marker element. + */ + function getCursorMarkerSpan( domLib, content ) { + return domLib( '<span>' ).css( { + display: 'inline-block', + width: 0, + overflow: 'hidden', + 'line-height': 0 + } ) + .html( content ? content : '' ); + } + + /** + * Gets adjusted selection cursor positions according to HTML tags, comments, and shortcodes. + * + * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render + * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible + * to break the syntax and render the HTML tag or shortcode broken. + * + * @link getShortcodeWrapperInfo + * + * @param {string} content Textarea content that the cursors are in + * @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions + * + * @return {{cursorStart: number, cursorEnd: number}} + */ + function adjustTextAreaSelectionCursors( content, cursorPositions ) { + var voidElements = [ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]; + + var cursorStart = cursorPositions.cursorStart, + cursorEnd = cursorPositions.cursorEnd, + // Check if the cursor is in a tag and if so, adjust it. + isCursorStartInTag = getContainingTagInfo( content, cursorStart ); + + if ( isCursorStartInTag ) { + /** + * Only move to the start of the HTML tag (to select the whole element) if the tag + * is part of the voidElements list above. + * + * This list includes tags that are self-contained and don't need a closing tag, according to the + * HTML5 specification. + * + * This is done in order to make selection of text a bit more consistent when selecting text in + * `<p>` tags or such. + * + * In cases where the tag is not a void element, the cursor is put to the end of the tag, + * so it's either between the opening and closing tag elements or after the closing tag. + */ + if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) { + cursorStart = isCursorStartInTag.ltPos; + } else { + cursorStart = isCursorStartInTag.gtPos; + } + } + + var isCursorEndInTag = getContainingTagInfo( content, cursorEnd ); + if ( isCursorEndInTag ) { + cursorEnd = isCursorEndInTag.gtPos; + } + + var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart ); + if ( isCursorStartInShortcode && ! isCursorStartInShortcode.showAsPlainText ) { + /** + * If a URL is at the start or the end of the content, + * the selection doesn't work, because it inserts a marker in the text, + * which breaks the embedURL detection. + * + * The best way to avoid that and not modify the user content is to + * adjust the cursor to either after or before URL. + */ + if ( isCursorStartInShortcode.urlAtStartOfContent ) { + cursorStart = isCursorStartInShortcode.endIndex; + } else { + cursorStart = isCursorStartInShortcode.startIndex; + } + } + + var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd ); + if ( isCursorEndInShortcode && ! isCursorEndInShortcode.showAsPlainText ) { + if ( isCursorEndInShortcode.urlAtEndOfContent ) { + cursorEnd = isCursorEndInShortcode.startIndex; + } else { + cursorEnd = isCursorEndInShortcode.endIndex; + } + } + + return { + cursorStart: cursorStart, + cursorEnd: cursorEnd + }; + } + + /** + * Adds text selection markers in the editor textarea. + * + * Adds selection markers in the content of the editor `textarea`. + * The method directly manipulates the `textarea` content, to allow TinyMCE plugins + * to run after the markers are added. + * + * @param {Object} $textarea TinyMCE's textarea wrapped as a DomQuery object + */ + function addHTMLBookmarkInTextAreaContent( $textarea ) { + if ( ! $textarea || ! $textarea.length ) { + // If no valid $textarea object is provided, there's nothing we can do. + return; + } + + var textArea = $textarea[0], + textAreaContent = textArea.value, + + adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, { + cursorStart: textArea.selectionStart, + cursorEnd: textArea.selectionEnd + } ), + + htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart, + htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd, + + mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single', + + selectedText = null, + cursorMarkerSkeleton = getCursorMarkerSpan( $$, '' ).attr( 'data-mce-type','bookmark' ); + + if ( mode === 'range' ) { + var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ), + bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' ); + + selectedText = [ + markedText, + bookMarkEnd[0].outerHTML + ].join( '' ); + } + + textArea.value = [ + textArea.value.slice( 0, htmlModeCursorStartPosition ), // Text until the cursor/selection position. + cursorMarkerSkeleton.clone() // Cursor/selection start marker. + .addClass( 'mce_SELRES_start' )[0].outerHTML, + selectedText, // Selected text with end cursor/position marker. + textArea.value.slice( htmlModeCursorEndPosition ) // Text from last cursor/selection position to end. + ].join( '' ); + } + + /** + * Focuses the selection markers in Visual mode. + * + * The method checks for existing selection markers inside the editor DOM (Visual mode) + * and create a selection between the two nodes using the DOM `createRange` selection API + * + * If there is only a single node, select only the single node through TinyMCE's selection API + * + * @param {Object} editor TinyMCE editor instance. + */ + function focusHTMLBookmarkInVisualEditor( editor ) { + var startNode = editor.$( '.mce_SELRES_start' ).attr( 'data-mce-bogus', 1 ), + endNode = editor.$( '.mce_SELRES_end' ).attr( 'data-mce-bogus', 1 ); + + if ( startNode.length ) { + editor.focus(); + + if ( ! endNode.length ) { + editor.selection.select( startNode[0] ); + } else { + var selection = editor.getDoc().createRange(); + + selection.setStartAfter( startNode[0] ); + selection.setEndBefore( endNode[0] ); + + editor.selection.setRng( selection ); + } + } + + if ( editor.getParam( 'wp_keep_scroll_position' ) ) { + scrollVisualModeToStartElement( editor, startNode ); + } + + removeSelectionMarker( startNode ); + removeSelectionMarker( endNode ); + + editor.save(); + } + + /** + * Removes selection marker and the parent node if it is an empty paragraph. + * + * By default TinyMCE wraps loose inline tags in a `<p>`. + * When removing selection markers an empty `<p>` may be left behind, remove it. + * + * @param {Object} $marker The marker to be removed from the editor DOM, wrapped in an instnce of `editor.$` + */ + function removeSelectionMarker( $marker ) { + var $markerParent = $marker.parent(); + + $marker.remove(); + + //Remove empty paragraph left over after removing the marker. + if ( $markerParent.is( 'p' ) && ! $markerParent.children().length && ! $markerParent.text() ) { + $markerParent.remove(); + } + } + + /** + * Scrolls the content to place the selected element in the center of the screen. + * + * Takes an element, that is usually the selection start element, selected in + * `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly + * in the middle of the screen. + * + * I order to achieve the proper positioning, the editor media bar and toolbar are subtracted + * from the window height, to get the proper viewport window, that the user sees. + * + * @param {Object} editor TinyMCE editor instance. + * @param {Object} element HTMLElement that should be scrolled into view. + */ + function scrollVisualModeToStartElement( editor, element ) { + var elementTop = editor.$( element ).offset().top, + TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top, + + toolbarHeight = getToolbarHeight( editor ), + + edTools = $( '#wp-content-editor-tools' ), + edToolsHeight = 0, + edToolsOffsetTop = 0, + + $scrollArea; + + if ( edTools.length ) { + edToolsHeight = edTools.height(); + edToolsOffsetTop = edTools.offset().top; + } + + var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + + selectionPosition = TinyMCEContentAreaTop + elementTop, + visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight ); + + // There's no need to scroll if the selection is inside the visible area. + if ( selectionPosition < visibleAreaHeight ) { + return; + } + + /** + * The minimum scroll height should be to the top of the editor, to offer a consistent + * experience. + * + * In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and + * subtracting the height. This gives the scroll position where the top of the editor tools aligns with + * the top of the viewport (under the Master Bar) + */ + var adjustedScroll; + if ( editor.settings.wp_autoresize_on ) { + $scrollArea = $( 'html,body' ); + adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight ); + } else { + $scrollArea = $( editor.contentDocument ).find( 'html,body' ); + adjustedScroll = elementTop; + } + + $scrollArea.animate( { + scrollTop: parseInt( adjustedScroll, 10 ) + }, 100 ); + } + + /** + * This method was extracted from the `SaveContent` hook in + * `wp-includes/js/tinymce/plugins/wordpress/plugin.js`. + * + * It's needed here, since the method changes the content a bit, which confuses the cursor position. + * + * @param {Object} event TinyMCE event object. + */ + function fixTextAreaContent( event ) { + // Keep empty paragraphs :( + event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p> </p>' ); + } + + /** + * Finds the current selection position in the Visual editor. + * + * Find the current selection in the Visual editor by inserting marker elements at the start + * and end of the selection. + * + * Uses the standard DOM selection API to achieve that goal. + * + * Check the notes in the comments in the code below for more information on some gotchas + * and why this solution was chosen. + * + * @param {Object} editor The editor where we must find the selection. + * @return {(null|Object)} The selection range position in the editor. + */ + function findBookmarkedPosition( editor ) { + // Get the TinyMCE `window` reference, since we need to access the raw selection. + var TinyMCEWindow = editor.getWin(), + selection = TinyMCEWindow.getSelection(); + + if ( ! selection || selection.rangeCount < 1 ) { + // no selection, no need to continue. + return; + } + + /** + * The ID is used to avoid replacing user generated content, that may coincide with the + * format specified below. + * @type {string} + */ + var selectionID = 'SELRES_' + Math.random(); + + /** + * Create two marker elements that will be used to mark the start and the end of the range. + * + * The elements have hardcoded style that makes them invisible. This is done to avoid seeing + * random content flickering in the editor when switching between modes. + */ + var spanSkeleton = getCursorMarkerSpan( editor.$, selectionID ), + startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ), + endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' ); + + /** + * Inspired by: + * @link https://stackoverflow.com/a/17497803/153310 + * + * Why do it this way and not with TinyMCE's bookmarks? + * + * TinyMCE's bookmarks are very nice when working with selections and positions, BUT + * there is no way to determine the precise position of the bookmark when switching modes, since + * TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify + * HTML code and so on. In this process, the bookmark markup gets lost. + * + * If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML + * in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will + * throw off the positioning. + * + * To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the + * selection. + * + * Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates + * a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to + * full nodes. Unfortunately in our case, the user can select whatever they like, which means that the + * selection may start in the middle of one node and end in the middle of a completely different one. If we + * wrap the selection in another node, this will create artifacts in the content. + * + * Using the method below, we insert the custom `span` nodes at the start and at the end of the selection. + * This helps us not break the content and also gives us the option to work with multi-node selections without + * breaking the markup. + */ + var range = selection.getRangeAt( 0 ), + startNode = range.startContainer, + startOffset = range.startOffset, + boundaryRange = range.cloneRange(); + + /** + * If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup, + * which we have to account for. + */ + if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) { + startNode = editor.$( '[data-mce-selected]' )[0]; + + /** + * Marking the start and end element with `data-mce-object-selection` helps + * discern when the selected object is a Live Preview selection. + * + * This way we can adjust the selection to properly select only the content, ignoring + * whitespace inserted around the selected object by the Editor. + */ + startElement.attr( 'data-mce-object-selection', 'true' ); + endElement.attr( 'data-mce-object-selection', 'true' ); + + editor.$( startNode ).before( startElement[0] ); + editor.$( startNode ).after( endElement[0] ); + } else { + boundaryRange.collapse( false ); + boundaryRange.insertNode( endElement[0] ); + + boundaryRange.setStart( startNode, startOffset ); + boundaryRange.collapse( true ); + boundaryRange.insertNode( startElement[0] ); + + range.setStartAfter( startElement[0] ); + range.setEndBefore( endElement[0] ); + selection.removeAllRanges(); + selection.addRange( range ); + } + + /** + * Now the editor's content has the start/end nodes. + * + * Unfortunately the content goes through some more changes after this step, before it gets inserted + * in the `textarea`. This means that we have to do some minor cleanup on our own here. + */ + editor.on( 'GetContent', fixTextAreaContent ); + + var content = removep( editor.getContent() ); + + editor.off( 'GetContent', fixTextAreaContent ); + + startElement.remove(); + endElement.remove(); + + var startRegex = new RegExp( + '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)' + ); + + var endRegex = new RegExp( + '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>' + ); + + var startMatch = content.match( startRegex ), + endMatch = content.match( endRegex ); + + if ( ! startMatch ) { + return null; + } + + var startIndex = startMatch.index, + startMatchLength = startMatch[0].length, + endIndex = null; + + if (endMatch) { + /** + * Adjust the selection index, if the selection contains a Live Preview object or not. + * + * Check where the `data-mce-object-selection` attribute is set above for more context. + */ + if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) { + startMatchLength -= startMatch[1].length; + } + + var endMatchIndex = endMatch.index; + + if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) { + endMatchIndex -= endMatch[1].length; + } + + // We need to adjust the end position to discard the length of the range start marker. + endIndex = endMatchIndex - startMatchLength; + } + + return { + start: startIndex, + end: endIndex + }; + } + + /** + * Selects text in the TinyMCE `textarea`. + * + * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`. + * + * For `selection` parameter: + * @link findBookmarkedPosition + * + * @param {Object} editor TinyMCE's editor instance. + * @param {Object} selection Selection data. + */ + function selectTextInTextArea( editor, selection ) { + // Only valid in the text area mode and if we have selection. + if ( ! selection ) { + return; + } + + var textArea = editor.getElement(), + start = selection.start, + end = selection.end || selection.start; + + if ( textArea.focus ) { + // Wait for the Visual editor to be hidden, then focus and scroll to the position. + setTimeout( function() { + textArea.setSelectionRange( start, end ); + if ( textArea.blur ) { + // Defocus before focusing. + textArea.blur(); + } + textArea.focus(); + }, 100 ); + } + } + + // Restore the selection when the editor is initialized. Needed when the Text editor is the default. + $( document ).on( 'tinymce-editor-init.keep-scroll-position', function( event, editor ) { + if ( editor.$( '.mce_SELRES_start' ).length ) { + focusHTMLBookmarkInVisualEditor( editor ); + } + } ); + + /** + * Replaces <p> tags with two line breaks. "Opposite" of wpautop(). + * + * Replaces <p> tags with two line breaks except where the <p> has attributes. + * Unifies whitespace. + * Indents <li>, <dt> and <dd> for better readability. + * + * @since 2.5.0 + * + * @memberof switchEditors + * + * @param {string} html The content from the editor. + * @return {string} The content with stripped paragraph tags. + */ + function removep( html ) { + var blocklist = 'blockquote|ul|ol|li|dl|dt|dd|table|thead|tbody|tfoot|tr|th|td|h[1-6]|fieldset|figure', + blocklist1 = blocklist + '|div|p', + blocklist2 = blocklist + '|pre', + preserve_linebreaks = false, + preserve_br = false, + preserve = []; + + if ( ! html ) { + return ''; + } + + // Protect script and style tags. + if ( html.indexOf( '<script' ) !== -1 || html.indexOf( '<style' ) !== -1 ) { + html = html.replace( /<(script|style)[^>]*>[\s\S]*?<\/\1>/g, function( match ) { + preserve.push( match ); + return '<wp-preserve>'; + } ); + } + + // Protect pre tags. + if ( html.indexOf( '<pre' ) !== -1 ) { + preserve_linebreaks = true; + html = html.replace( /<pre[^>]*>[\s\S]+?<\/pre>/g, function( a ) { + a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' ); + a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' ); + return a.replace( /\r?\n/g, '<wp-line-break>' ); + }); + } + + // Remove line breaks but keep <br> tags inside image captions. + if ( html.indexOf( '[caption' ) !== -1 ) { + preserve_br = true; + html = html.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) { + return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' ); + }); + } + + // Normalize white space characters before and after block tags. + html = html.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' ); + html = html.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' ); + + // Mark </p> if it has any attributes. + html = html.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' ); + + // Preserve the first <p> inside a <div>. + html = html.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' ); + + // Remove paragraph tags. + html = html.replace( /\s*<p>/gi, '' ); + html = html.replace( /\s*<\/p>\s*/gi, '\n\n' ); + + // Normalize white space chars and remove multiple line breaks. + html = html.replace( /\n[\s\u00a0]+\n/g, '\n\n' ); + + // Replace <br> tags with line breaks. + html = html.replace( /(\s*)<br ?\/?>\s*/gi, function( match, space ) { + if ( space && space.indexOf( '\n' ) !== -1 ) { + return '\n\n'; + } + + return '\n'; + }); + + // Fix line breaks around <div>. + html = html.replace( /\s*<div/g, '\n<div' ); + html = html.replace( /<\/div>\s*/g, '</div>\n' ); + + // Fix line breaks around caption shortcodes. + html = html.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' ); + html = html.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' ); + + // Pad block elements tags with a line break. + html = html.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' ); + html = html.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' ); + + // Indent <li>, <dt> and <dd> tags. + html = html.replace( /<((li|dt|dd)[^>]*)>/g, ' \t<$1>' ); + + // Fix line breaks around <select> and <option>. + if ( html.indexOf( '<option' ) !== -1 ) { + html = html.replace( /\s*<option/g, '\n<option' ); + html = html.replace( /\s*<\/select>/g, '\n</select>' ); + } + + // Pad <hr> with two line breaks. + if ( html.indexOf( '<hr' ) !== -1 ) { + html = html.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' ); + } + + // Remove line breaks in <object> tags. + if ( html.indexOf( '<object' ) !== -1 ) { + html = html.replace( /<object[\s\S]+?<\/object>/g, function( a ) { + return a.replace( /[\r\n]+/g, '' ); + }); + } + + // Unmark special paragraph closing tags. + html = html.replace( /<\/p#>/g, '</p>\n' ); + + // Pad remaining <p> tags whit a line break. + html = html.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' ); + + // Trim. + html = html.replace( /^\s+/, '' ); + html = html.replace( /[\s\u00a0]+$/, '' ); + + if ( preserve_linebreaks ) { + html = html.replace( /<wp-line-break>/g, '\n' ); + } + + if ( preserve_br ) { + html = html.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' ); + } + + // Restore preserved tags. + if ( preserve.length ) { + html = html.replace( /<wp-preserve>/g, function() { + return preserve.shift(); + } ); + } + + return html; + } + + /** + * Replaces two line breaks with a paragraph tag and one line break with a <br>. + * + * Similar to `wpautop()` in formatting.php. + * + * @since 2.5.0 + * + * @memberof switchEditors + * + * @param {string} text The text input. + * @return {string} The formatted text. + */ + function autop( text ) { + var preserve_linebreaks = false, + preserve_br = false, + blocklist = 'table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre' + + '|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section' + + '|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary'; + + // Normalize line breaks. + text = text.replace( /\r\n|\r/g, '\n' ); + + // Remove line breaks from <object>. + if ( text.indexOf( '<object' ) !== -1 ) { + text = text.replace( /<object[\s\S]+?<\/object>/g, function( a ) { + return a.replace( /\n+/g, '' ); + }); + } + + // Remove line breaks from tags. + text = text.replace( /<[^<>]+>/g, function( a ) { + return a.replace( /[\n\t ]+/g, ' ' ); + }); + + // Preserve line breaks in <pre> and <script> tags. + if ( text.indexOf( '<pre' ) !== -1 || text.indexOf( '<script' ) !== -1 ) { + preserve_linebreaks = true; + text = text.replace( /<(pre|script)[^>]*>[\s\S]*?<\/\1>/g, function( a ) { + return a.replace( /\n/g, '<wp-line-break>' ); + }); + } + + if ( text.indexOf( '<figcaption' ) !== -1 ) { + text = text.replace( /\s*(<figcaption[^>]*>)/g, '$1' ); + text = text.replace( /<\/figcaption>\s*/g, '</figcaption>' ); + } + + // Keep <br> tags inside captions. + if ( text.indexOf( '[caption' ) !== -1 ) { + preserve_br = true; + + text = text.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) { + a = a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ); + + a = a.replace( /<[^<>]+>/g, function( b ) { + return b.replace( /[\n\t ]+/, ' ' ); + }); + + return a.replace( /\s*\n\s*/g, '<wp-temp-br />' ); + }); + } + + text = text + '\n\n'; + text = text.replace( /<br \/>\s*<br \/>/gi, '\n\n' ); + + // Pad block tags with two line breaks. + text = text.replace( new RegExp( '(<(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '\n\n$1' ); + text = text.replace( new RegExp( '(</(?:' + blocklist + ')>)', 'gi' ), '$1\n\n' ); + text = text.replace( /<hr( [^>]*)?>/gi, '<hr$1>\n\n' ); + + // Remove white space chars around <option>. + text = text.replace( /\s*<option/gi, '<option' ); + text = text.replace( /<\/option>\s*/gi, '</option>' ); + + // Normalize multiple line breaks and white space chars. + text = text.replace( /\n\s*\n+/g, '\n\n' ); + + // Convert two line breaks to a paragraph. + text = text.replace( /([\s\S]+?)\n\n/g, '<p>$1</p>\n' ); + + // Remove empty paragraphs. + text = text.replace( /<p>\s*?<\/p>/gi, ''); + + // Remove <p> tags that are around block tags. + text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' ); + text = text.replace( /<p>(<li.+?)<\/p>/gi, '$1'); + + // Fix <p> in blockquotes. + text = text.replace( /<p>\s*<blockquote([^>]*)>/gi, '<blockquote$1><p>'); + text = text.replace( /<\/blockquote>\s*<\/p>/gi, '</p></blockquote>'); + + // Remove <p> tags that are wrapped around block tags. + text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '$1' ); + text = text.replace( new RegExp( '(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' ); + + text = text.replace( /(<br[^>]*>)\s*\n/gi, '$1' ); + + // Add <br> tags. + text = text.replace( /\s*\n/g, '<br />\n'); + + // Remove <br> tags that are around block tags. + text = text.replace( new RegExp( '(</?(?:' + blocklist + ')[^>]*>)\\s*<br />', 'gi' ), '$1' ); + text = text.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, '$1' ); + + // Remove <p> and <br> around captions. + text = text.replace( /(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi, '[caption$1[/caption]' ); + + // Make sure there is <p> when there is </p> inside block tags that can contain other blocks. + text = text.replace( /(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function( a, b, c ) { + if ( c.match( /<p( [^>]*)?>/ ) ) { + return a; + } + + return b + '<p>' + c + '</p>'; + }); + + // Restore the line breaks in <pre> and <script> tags. + if ( preserve_linebreaks ) { + text = text.replace( /<wp-line-break>/g, '\n' ); + } + + // Restore the <br> tags in captions. + if ( preserve_br ) { + text = text.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' ); + } + + return text; + } + + /** + * Fires custom jQuery events `beforePreWpautop` and `afterPreWpautop` when jQuery is available. + * + * @since 2.9.0 + * + * @memberof switchEditors + * + * @param {string} html The content from the visual editor. + * @return {string} the filtered content. + */ + function pre_wpautop( html ) { + var obj = { o: exports, data: html, unfiltered: html }; + + if ( $ ) { + $( 'body' ).trigger( 'beforePreWpautop', [ obj ] ); + } + + obj.data = removep( obj.data ); + + if ( $ ) { + $( 'body' ).trigger( 'afterPreWpautop', [ obj ] ); + } + + return obj.data; + } + + /** + * Fires custom jQuery events `beforeWpautop` and `afterWpautop` when jQuery is available. + * + * @since 2.9.0 + * + * @memberof switchEditors + * + * @param {string} text The content from the text editor. + * @return {string} filtered content. + */ + function wpautop( text ) { + var obj = { o: exports, data: text, unfiltered: text }; + + if ( $ ) { + $( 'body' ).trigger( 'beforeWpautop', [ obj ] ); + } + + obj.data = autop( obj.data ); + + if ( $ ) { + $( 'body' ).trigger( 'afterWpautop', [ obj ] ); + } + + return obj.data; + } + + if ( $ ) { + $( init ); + } else if ( document.addEventListener ) { + document.addEventListener( 'DOMContentLoaded', init, false ); + window.addEventListener( 'load', init, false ); + } else if ( window.attachEvent ) { + window.attachEvent( 'onload', init ); + document.attachEvent( 'onreadystatechange', function() { + if ( 'complete' === document.readyState ) { + init(); + } + } ); + } + + wp.editor.autop = wpautop; + wp.editor.removep = pre_wpautop; + + exports = { + go: switchEditor, + wpautop: wpautop, + pre_wpautop: pre_wpautop, + _wp_Autop: autop, + _wp_Nop: removep + }; + + return exports; + } + + /** + * Expose the switch editors to be used globally. + * + * @namespace switchEditors + */ + window.switchEditors = new SwitchEditors(); + + /** + * Initialize TinyMCE and/or Quicktags. For use with wp_enqueue_editor() (PHP). + * + * Intended for use with an existing textarea that will become the Text editor tab. + * The editor width will be the width of the textarea container, height will be adjustable. + * + * Settings for both TinyMCE and Quicktags can be passed on initialization, and are "filtered" + * with custom jQuery events on the document element, wp-before-tinymce-init and wp-before-quicktags-init. + * + * @since 4.8.0 + * + * @param {string} id The HTML id of the textarea that is used for the editor. + * Has to be jQuery compliant. No brackets, special chars, etc. + * @param {Object} settings Example: + * settings = { + * // See https://www.tinymce.com/docs/configure/integration-and-setup/. + * // Alternatively set to `true` to use the defaults. + * tinymce: { + * setup: function( editor ) { + * console.log( 'Editor initialized', editor ); + * } + * } + * + * // Alternatively set to `true` to use the defaults. + * quicktags: { + * buttons: 'strong,em,link' + * } + * } + */ + wp.editor.initialize = function( id, settings ) { + var init; + var defaults; + + if ( ! $ || ! id || ! wp.editor.getDefaultSettings ) { + return; + } + + defaults = wp.editor.getDefaultSettings(); + + // Initialize TinyMCE by default. + if ( ! settings ) { + settings = { + tinymce: true + }; + } + + // Add wrap and the Visual|Text tabs. + if ( settings.tinymce && settings.quicktags ) { + var $textarea = $( '#' + id ); + + var $wrap = $( '<div>' ).attr( { + 'class': 'wp-core-ui wp-editor-wrap tmce-active', + id: 'wp-' + id + '-wrap' + } ); + + var $editorContainer = $( '<div class="wp-editor-container">' ); + + var $button = $( '<button>' ).attr( { + type: 'button', + 'data-wp-editor-id': id + } ); + + var $editorTools = $( '<div class="wp-editor-tools">' ); + + if ( settings.mediaButtons ) { + var buttonText = 'Add Media'; + + if ( window._wpMediaViewsL10n && window._wpMediaViewsL10n.addMedia ) { + buttonText = window._wpMediaViewsL10n.addMedia; + } + + var $addMediaButton = $( '<button type="button" class="button insert-media add_media">' ); + + $addMediaButton.append( '<span class="wp-media-buttons-icon"></span>' ); + $addMediaButton.append( document.createTextNode( ' ' + buttonText ) ); + $addMediaButton.data( 'editor', id ); + + $editorTools.append( + $( '<div class="wp-media-buttons">' ) + .append( $addMediaButton ) + ); + } + + $wrap.append( + $editorTools + .append( $( '<div class="wp-editor-tabs">' ) + .append( $button.clone().attr({ + id: id + '-tmce', + 'class': 'wp-switch-editor switch-tmce' + }).text( window.tinymce.translate( 'Visual' ) ) ) + .append( $button.attr({ + id: id + '-html', + 'class': 'wp-switch-editor switch-html' + }).text( window.tinymce.translate( 'Text' ) ) ) + ).append( $editorContainer ) + ); + + $textarea.after( $wrap ); + $editorContainer.append( $textarea ); + } + + if ( window.tinymce && settings.tinymce ) { + if ( typeof settings.tinymce !== 'object' ) { + settings.tinymce = {}; + } + + init = $.extend( {}, defaults.tinymce, settings.tinymce ); + init.selector = '#' + id; + + $( document ).trigger( 'wp-before-tinymce-init', init ); + window.tinymce.init( init ); + + if ( ! window.wpActiveEditor ) { + window.wpActiveEditor = id; + } + } + + if ( window.quicktags && settings.quicktags ) { + if ( typeof settings.quicktags !== 'object' ) { + settings.quicktags = {}; + } + + init = $.extend( {}, defaults.quicktags, settings.quicktags ); + init.id = id; + + $( document ).trigger( 'wp-before-quicktags-init', init ); + window.quicktags( init ); + + if ( ! window.wpActiveEditor ) { + window.wpActiveEditor = init.id; + } + } + }; + + /** + * Remove one editor instance. + * + * Intended for use with editors that were initialized with wp.editor.initialize(). + * + * @since 4.8.0 + * + * @param {string} id The HTML id of the editor textarea. + */ + wp.editor.remove = function( id ) { + var mceInstance, qtInstance, + $wrap = $( '#wp-' + id + '-wrap' ); + + if ( window.tinymce ) { + mceInstance = window.tinymce.get( id ); + + if ( mceInstance ) { + if ( ! mceInstance.isHidden() ) { + mceInstance.save(); + } + + mceInstance.remove(); + } + } + + if ( window.quicktags ) { + qtInstance = window.QTags.getInstance( id ); + + if ( qtInstance ) { + qtInstance.remove(); + } + } + + if ( $wrap.length ) { + $wrap.after( $( '#' + id ) ); + $wrap.remove(); + } + }; + + /** + * Get the editor content. + * + * Intended for use with editors that were initialized with wp.editor.initialize(). + * + * @since 4.8.0 + * + * @param {string} id The HTML id of the editor textarea. + * @return The editor content. + */ + wp.editor.getContent = function( id ) { + var editor; + + if ( ! $ || ! id ) { + return; + } + + if ( window.tinymce ) { + editor = window.tinymce.get( id ); + + if ( editor && ! editor.isHidden() ) { + editor.save(); + } + } + + return $( '#' + id ).val(); + }; + +}( window.jQuery, window.wp )); diff --git a/wp-admin/js/editor.min.js b/wp-admin/js/editor.min.js new file mode 100644 index 0000000..8e7adc4 --- /dev/null +++ b/wp-admin/js/editor.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},function(g,u){u.editor=u.editor||{},window.switchEditors=new function(){var h,b,t={};function e(){!h&&window.tinymce&&(h=window.tinymce,(b=h.$)(document).on("click",function(e){e=b(e.target);e.hasClass("wp-switch-editor")&&n(e.attr("data-wp-editor-id"),e.hasClass("switch-tmce")?"tmce":"html")}))}function v(e){e=b(".mce-toolbar-grp",e.getContainer())[0],e=e&&e.clientHeight;return e&&10<e&&e<200?parseInt(e,10):30}function n(e,t){t=t||"toggle";var n,i,r,a,o,c,p,s,d,l,g=h.get(e=e||"content"),u=b("#wp-"+e+"-wrap"),w=b("#"+e),m=w[0];if("tmce"===(t="toggle"===t?g&&!g.isHidden()?"html":"tmce":t)||"tinymce"===t){if(g&&!g.isHidden())return!1;void 0!==window.QTags&&window.QTags.closeAllTags(e);var f=parseInt(m.style.height,10)||0;(g?g.getParam("wp_keep_scroll_position"):window.tinyMCEPreInit.mceInit[e]&&window.tinyMCEPreInit.mceInit[e].wp_keep_scroll_position)&&(a=w)&&a.length&&(a=a[0],c=function(e,t){var n=t.cursorStart,t=t.cursorEnd,i=x(e,n);i&&(n=-1!==["area","base","br","col","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"].indexOf(i.tagType)?i.ltPos:i.gtPos);i=x(e,t);i&&(t=i.gtPos);i=E(e,n);i&&!i.showAsPlainText&&(n=i.urlAtStartOfContent?i.endIndex:i.startIndex);i=E(e,t);i&&!i.showAsPlainText&&(t=i.urlAtEndOfContent?i.startIndex:i.endIndex);return{cursorStart:n,cursorEnd:t}}(a.value,{cursorStart:a.selectionStart,cursorEnd:a.selectionEnd}),o=c.cursorStart,c=c.cursorEnd,d=o!==c?"range":"single",p=null,s=y(b,"").attr("data-mce-type","bookmark"),"range"==d&&(d=a.value.slice(o,c),l=s.clone().addClass("mce_SELRES_end"),p=[d,l[0].outerHTML].join("")),a.value=[a.value.slice(0,o),s.clone().addClass("mce_SELRES_start")[0].outerHTML,p,a.value.slice(c)].join("")),g?(g.show(),!h.Env.iOS&&f&&50<(f=f-v(g)+14)&&f<5e3&&g.theme.resizeTo(null,f),g.getParam("wp_keep_scroll_position")&&S(g)):h.init(window.tinyMCEPreInit.mceInit[e]),u.removeClass("html-active").addClass("tmce-active"),w.attr("aria-hidden",!0),window.setUserSetting("editor","tinymce")}else if("html"===t){if(g&&g.isHidden())return!1;g?(h.Env.iOS||(f=(d=g.iframeElement)?parseInt(d.style.height,10):0)&&50<(f=f+v(g)-14)&&f<5e3&&(m.style.height=f+"px"),l=null,g.getParam("wp_keep_scroll_position")&&(l=function(e){var t,n,i,r,a,o,c,p=e.getWin().getSelection();if(p&&!(p.rangeCount<1))return c="SELRES_"+Math.random(),o=y(e.$,c),a=o.clone().addClass("mce_SELRES_start"),o=o.clone().addClass("mce_SELRES_end"),r=p.getRangeAt(0),t=r.startContainer,n=r.startOffset,i=r.cloneRange(),0<e.$(t).parents(".mce-offscreen-selection").length?(t=e.$("[data-mce-selected]")[0],a.attr("data-mce-object-selection","true"),o.attr("data-mce-object-selection","true"),e.$(t).before(a[0]),e.$(t).after(o[0])):(i.collapse(!1),i.insertNode(o[0]),i.setStart(t,n),i.collapse(!0),i.insertNode(a[0]),r.setStartAfter(a[0]),r.setEndBefore(o[0]),p.removeAllRanges(),p.addRange(r)),e.on("GetContent",_),t=$(e.getContent()),e.off("GetContent",_),a.remove(),o.remove(),n=new RegExp('<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*'+c+"[^<]*<\\/span>(\\s*)"),i=new RegExp('(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*'+c+"[^<]*<\\/span>"),p=t.match(n),r=t.match(i),p?(e=p.index,a=p[0].length,o=null,r&&(-1!==p[0].indexOf("data-mce-object-selection")&&(a-=p[1].length),c=r.index,-1!==r[0].indexOf("data-mce-object-selection")&&(c-=r[1].length),o=c-a),{start:e,end:o}):null}(g)),g.hide(),l&&(o=g,s=l)&&(n=o.getElement(),i=s.start,r=s.end||s.start,n.focus)&&setTimeout(function(){n.setSelectionRange(i,r),n.blur&&n.blur(),n.focus()},100)):w.css({display:"",visibility:""}),u.removeClass("tmce-active").addClass("html-active"),w.attr("aria-hidden",!1),window.setUserSetting("editor","html")}}function x(e,t){var n,i=e.lastIndexOf("<",t-1);return(e.lastIndexOf(">",t)<i||">"===e.substr(t,1))&&(e=(t=e.substr(i)).match(/<\s*(\/)?(\w+|\!-{2}.*-{2})/))?(n=e[2],{ltPos:i,gtPos:i+t.indexOf(">")+1,tagType:n,isClosingTag:!!e[1]}):null}function E(e,t){for(var n=function(e){var t,n=function(e){var t=e.match(/\[+([\w_-])+/g),n=[];if(t)for(var i=0;i<t.length;i++){var r=t[i].replace(/^\[+/g,"");-1===n.indexOf(r)&&n.push(r)}return n}(e);if(0===n.length)return[];var i,r=u.shortcode.regexp(n.join("|")),a=[];for(;i=r.exec(e);){var o="["===i[1];t={shortcodeName:i[2],showAsPlainText:o,startIndex:i.index,endIndex:i.index+i[0].length,length:i[0].length},a.push(t)}var c=new RegExp('(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^s"]+?)(<\\/p>s*|[\\n\\r][\\n\\r]|$)',"gi");for(;i=c.exec(e);)t={shortcodeName:"url",showAsPlainText:!1,startIndex:i.index,endIndex:i.index+i[0].length,length:i[0].length,urlAtStartOfContent:""===i[1],urlAtEndOfContent:""===i[3]},a.push(t);return a}(e),i=0;i<n.length;i++){var r=n[i];if(t>=r.startIndex&&t<=r.endIndex)return r}}function y(e,t){return e("<span>").css({display:"inline-block",width:0,overflow:"hidden","line-height":0}).html(t||"")}function S(e){var t,n,i,r,a,o,c,p,s=e.$(".mce_SELRES_start").attr("data-mce-bogus",1),d=e.$(".mce_SELRES_end").attr("data-mce-bogus",1);s.length&&(e.focus(),d.length?((i=e.getDoc().createRange()).setStartAfter(s[0]),i.setEndBefore(d[0]),e.selection.setRng(i)):e.selection.select(s[0])),e.getParam("wp_keep_scroll_position")&&(i=s,i=(t=e).$(i).offset().top,r=t.$(t.getContentAreaContainer()).offset().top,a=v(t),o=g("#wp-content-editor-tools"),p=c=0,o.length&&(c=o.height(),p=o.offset().top),o=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight,(r+=i)<(o-=c+a)||(a=t.settings.wp_autoresize_on?(n=g("html,body"),Math.max(r-o/2,p-c)):(n=g(t.contentDocument).find("html,body"),i),n.animate({scrollTop:parseInt(a,10)},100))),l(s),l(d),e.save()}function l(e){var t=e.parent();e.remove(),!t.is("p")||t.children().length||t.text()||t.remove()}function _(e){e.content=e.content.replace(/<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g,"<p> </p>")}function $(e){var t="blockquote|ul|ol|li|dl|dt|dd|table|thead|tbody|tfoot|tr|th|td|h[1-6]|fieldset|figure",n=t+"|div|p",t=t+"|pre",i=!1,r=!1,a=[];return e?(-1!==(e=-1===e.indexOf("<script")&&-1===e.indexOf("<style")?e:e.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/g,function(e){return a.push(e),"<wp-preserve>"})).indexOf("<pre")&&(i=!0,e=e.replace(/<pre[^>]*>[\s\S]+?<\/pre>/g,function(e){return(e=(e=e.replace(/<br ?\/?>(\r\n|\n)?/g,"<wp-line-break>")).replace(/<\/?p( [^>]*)?>(\r\n|\n)?/g,"<wp-line-break>")).replace(/\r?\n/g,"<wp-line-break>")})),-1!==e.indexOf("[caption")&&(r=!0,e=e.replace(/\[caption[\s\S]+?\[\/caption\]/g,function(e){return e.replace(/<br([^>]*)>/g,"<wp-temp-br$1>").replace(/[\r\n\t]+/,"")})),e=(e=(e=(e=(e=-1!==(e=-1!==(e=-1!==(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=e.replace(new RegExp("\\s*</("+n+")>\\s*","g"),"</$1>\n")).replace(new RegExp("\\s*<((?:"+n+")(?: [^>]*)?)>","g"),"\n<$1>")).replace(/(<p [^>]+>.*?)<\/p>/g,"$1</p#>")).replace(/<div( [^>]*)?>\s*<p>/gi,"<div$1>\n\n")).replace(/\s*<p>/gi,"")).replace(/\s*<\/p>\s*/gi,"\n\n")).replace(/\n[\s\u00a0]+\n/g,"\n\n")).replace(/(\s*)<br ?\/?>\s*/gi,function(e,t){return t&&-1!==t.indexOf("\n")?"\n\n":"\n"})).replace(/\s*<div/g,"\n<div")).replace(/<\/div>\s*/g,"</div>\n")).replace(/\s*\[caption([^\[]+)\[\/caption\]\s*/gi,"\n\n[caption$1[/caption]\n\n")).replace(/caption\]\n\n+\[caption/g,"caption]\n\n[caption")).replace(new RegExp("\\s*<((?:"+t+")(?: [^>]*)?)\\s*>","g"),"\n<$1>")).replace(new RegExp("\\s*</("+t+")>\\s*","g"),"</$1>\n")).replace(/<((li|dt|dd)[^>]*)>/g," \t<$1>")).indexOf("<option")?(e=e.replace(/\s*<option/g,"\n<option")).replace(/\s*<\/select>/g,"\n</select>"):e).indexOf("<hr")?e.replace(/\s*<hr( [^>]*)?>\s*/g,"\n\n<hr$1>\n\n"):e).indexOf("<object")?e.replace(/<object[\s\S]+?<\/object>/g,function(e){return e.replace(/[\r\n]+/g,"")}):e).replace(/<\/p#>/g,"</p>\n")).replace(/\s*(<p [^>]+>[\s\S]*?<\/p>)/g,"\n$1")).replace(/^\s+/,"")).replace(/[\s\u00a0]+$/,""),i&&(e=e.replace(/<wp-line-break>/g,"\n")),r&&(e=e.replace(/<wp-temp-br([^>]*)>/g,"<br$1>")),a.length?e.replace(/<wp-preserve>/g,function(){return a.shift()}):e):""}function i(e){var t=!1,n=!1,i="table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary";return-1===(e=(e=-1!==(e=e.replace(/\r\n|\r/g,"\n")).indexOf("<object")?e.replace(/<object[\s\S]+?<\/object>/g,function(e){return e.replace(/\n+/g,"")}):e).replace(/<[^<>]+>/g,function(e){return e.replace(/[\n\t ]+/g," ")})).indexOf("<pre")&&-1===e.indexOf("<script")||(t=!0,e=e.replace(/<(pre|script)[^>]*>[\s\S]*?<\/\1>/g,function(e){return e.replace(/\n/g,"<wp-line-break>")})),-1!==(e=-1!==e.indexOf("<figcaption")?(e=e.replace(/\s*(<figcaption[^>]*>)/g,"$1")).replace(/<\/figcaption>\s*/g,"</figcaption>"):e).indexOf("[caption")&&(n=!0,e=e.replace(/\[caption[\s\S]+?\[\/caption\]/g,function(e){return(e=(e=e.replace(/<br([^>]*)>/g,"<wp-temp-br$1>")).replace(/<[^<>]+>/g,function(e){return e.replace(/[\n\t ]+/," ")})).replace(/\s*\n\s*/g,"<wp-temp-br />")})),e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e+="\n\n").replace(/<br \/>\s*<br \/>/gi,"\n\n")).replace(new RegExp("(<(?:"+i+")(?: [^>]*)?>)","gi"),"\n\n$1")).replace(new RegExp("(</(?:"+i+")>)","gi"),"$1\n\n")).replace(/<hr( [^>]*)?>/gi,"<hr$1>\n\n")).replace(/\s*<option/gi,"<option")).replace(/<\/option>\s*/gi,"</option>")).replace(/\n\s*\n+/g,"\n\n")).replace(/([\s\S]+?)\n\n/g,"<p>$1</p>\n")).replace(/<p>\s*?<\/p>/gi,"")).replace(new RegExp("<p>\\s*(</?(?:"+i+")(?: [^>]*)?>)\\s*</p>","gi"),"$1")).replace(/<p>(<li.+?)<\/p>/gi,"$1")).replace(/<p>\s*<blockquote([^>]*)>/gi,"<blockquote$1><p>")).replace(/<\/blockquote>\s*<\/p>/gi,"</p></blockquote>")).replace(new RegExp("<p>\\s*(</?(?:"+i+")(?: [^>]*)?>)","gi"),"$1")).replace(new RegExp("(</?(?:"+i+")(?: [^>]*)?>)\\s*</p>","gi"),"$1")).replace(/(<br[^>]*>)\s*\n/gi,"$1")).replace(/\s*\n/g,"<br />\n")).replace(new RegExp("(</?(?:"+i+")[^>]*>)\\s*<br />","gi"),"$1")).replace(/<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi,"$1")).replace(/(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi,"[caption$1[/caption]")).replace(/(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g,function(e,t,n){return n.match(/<p( [^>]*)?>/)?e:t+"<p>"+n+"</p>"}),t&&(e=e.replace(/<wp-line-break>/g,"\n")),e=n?e.replace(/<wp-temp-br([^>]*)>/g,"<br$1>"):e}function r(e){e={o:t,data:e,unfiltered:e};return g&&g("body").trigger("beforePreWpautop",[e]),e.data=$(e.data),g&&g("body").trigger("afterPreWpautop",[e]),e.data}function a(e){e={o:t,data:e,unfiltered:e};return g&&g("body").trigger("beforeWpautop",[e]),e.data=i(e.data),g&&g("body").trigger("afterWpautop",[e]),e.data}return g(document).on("tinymce-editor-init.keep-scroll-position",function(e,t){t.$(".mce_SELRES_start").length&&S(t)}),g?g(e):document.addEventListener?(document.addEventListener("DOMContentLoaded",e,!1),window.addEventListener("load",e,!1)):window.attachEvent&&(window.attachEvent("onload",e),document.attachEvent("onreadystatechange",function(){"complete"===document.readyState&&e()})),u.editor.autop=a,u.editor.removep=r,t={go:n,wpautop:a,pre_wpautop:r,_wp_Autop:i,_wp_Nop:$}},u.editor.initialize=function(e,t){var n,i,r,a,o,c,p,s,d;g&&e&&u.editor.getDefaultSettings&&(d=u.editor.getDefaultSettings(),(t=t||{tinymce:!0}).tinymce&&t.quicktags&&(i=g("#"+e),r=g("<div>").attr({class:"wp-core-ui wp-editor-wrap tmce-active",id:"wp-"+e+"-wrap"}),a=g('<div class="wp-editor-container">'),o=g("<button>").attr({type:"button","data-wp-editor-id":e}),c=g('<div class="wp-editor-tools">'),t.mediaButtons&&(p="Add Media",window._wpMediaViewsL10n&&window._wpMediaViewsL10n.addMedia&&(p=window._wpMediaViewsL10n.addMedia),(s=g('<button type="button" class="button insert-media add_media">')).append('<span class="wp-media-buttons-icon"></span>'),s.append(document.createTextNode(" "+p)),s.data("editor",e),c.append(g('<div class="wp-media-buttons">').append(s))),r.append(c.append(g('<div class="wp-editor-tabs">').append(o.clone().attr({id:e+"-tmce",class:"wp-switch-editor switch-tmce"}).text(window.tinymce.translate("Visual"))).append(o.attr({id:e+"-html",class:"wp-switch-editor switch-html"}).text(window.tinymce.translate("Text")))).append(a)),i.after(r),a.append(i)),window.tinymce&&t.tinymce&&("object"!=typeof t.tinymce&&(t.tinymce={}),(n=g.extend({},d.tinymce,t.tinymce)).selector="#"+e,g(document).trigger("wp-before-tinymce-init",n),window.tinymce.init(n),window.wpActiveEditor||(window.wpActiveEditor=e)),window.quicktags)&&t.quicktags&&("object"!=typeof t.quicktags&&(t.quicktags={}),(n=g.extend({},d.quicktags,t.quicktags)).id=e,g(document).trigger("wp-before-quicktags-init",n),window.quicktags(n),window.wpActiveEditor||(window.wpActiveEditor=n.id))},u.editor.remove=function(e){var t,n=g("#wp-"+e+"-wrap");window.tinymce&&(t=window.tinymce.get(e))&&(t.isHidden()||t.save(),t.remove()),window.quicktags&&(t=window.QTags.getInstance(e))&&t.remove(),n.length&&(n.after(g("#"+e)),n.remove())},u.editor.getContent=function(e){var t;if(g&&e)return window.tinymce&&(t=window.tinymce.get(e))&&!t.isHidden()&&t.save(),g("#"+e).val()}}(window.jQuery,window.wp);
\ No newline at end of file diff --git a/wp-admin/js/farbtastic.js b/wp-admin/js/farbtastic.js new file mode 100644 index 0000000..b445081 --- /dev/null +++ b/wp-admin/js/farbtastic.js @@ -0,0 +1,282 @@ +/*! + * Farbtastic: jQuery color picker plug-in v1.3u + * https://github.com/mattfarina/farbtastic + * + * Licensed under the GPL license: + * http://www.gnu.org/licenses/gpl.html + */ +/** + * Modified for WordPress: replaced deprecated jQuery methods. + * See https://core.trac.wordpress.org/ticket/57946. + */ + +(function($) { + +$.fn.farbtastic = function (options) { + $.farbtastic(this, options); + return this; +}; + +$.farbtastic = function (container, callback) { + var container = $(container).get(0); + return container.farbtastic || (container.farbtastic = new $._farbtastic(container, callback)); +}; + +$._farbtastic = function (container, callback) { + // Store farbtastic object + var fb = this; + + // Insert markup + $(container).html('<div class="farbtastic"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>'); + var e = $('.farbtastic', container); + fb.wheel = $('.wheel', container).get(0); + // Dimensions + fb.radius = 84; + fb.square = 100; + fb.width = 194; + + // Fix background PNGs in IE6 + if (navigator.appVersion.match(/MSIE [0-6]\./)) { + $('*', e).each(function () { + if (this.currentStyle.backgroundImage != 'none') { + var image = this.currentStyle.backgroundImage; + image = this.currentStyle.backgroundImage.substring(5, image.length - 2); + $(this).css({ + 'backgroundImage': 'none', + 'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')" + }); + } + }); + } + + /** + * Link to the given element(s) or callback. + */ + fb.linkTo = function (callback) { + // Unbind previous nodes + if (typeof fb.callback == 'object') { + $(fb.callback).off('keyup', fb.updateValue); + } + + // Reset color + fb.color = null; + + // Bind callback or elements + if (typeof callback == 'function') { + fb.callback = callback; + } + else if (typeof callback == 'object' || typeof callback == 'string') { + fb.callback = $(callback); + fb.callback.on('keyup', fb.updateValue); + if (fb.callback.get(0).value) { + fb.setColor(fb.callback.get(0).value); + } + } + return this; + }; + fb.updateValue = function (event) { + if (this.value && this.value != fb.color) { + fb.setColor(this.value); + } + }; + + /** + * Change color with HTML syntax #123456 + */ + fb.setColor = function (color) { + var unpack = fb.unpack(color); + if (fb.color != color && unpack) { + fb.color = color; + fb.rgb = unpack; + fb.hsl = fb.RGBToHSL(fb.rgb); + fb.updateDisplay(); + } + return this; + }; + + /** + * Change color with HSL triplet [0..1, 0..1, 0..1] + */ + fb.setHSL = function (hsl) { + fb.hsl = hsl; + fb.rgb = fb.HSLToRGB(hsl); + fb.color = fb.pack(fb.rgb); + fb.updateDisplay(); + return this; + }; + + ///////////////////////////////////////////////////// + + /** + * Retrieve the coordinates of the given event relative to the center + * of the widget. + */ + fb.widgetCoords = function (event) { + var offset = $(fb.wheel).offset(); + return { x: (event.pageX - offset.left) - fb.width / 2, y: (event.pageY - offset.top) - fb.width / 2 }; + }; + + /** + * Mousedown handler + */ + fb.mousedown = function (event) { + // Capture mouse + if (!document.dragging) { + $(document).on('mousemove', fb.mousemove).on('mouseup', fb.mouseup); + document.dragging = true; + } + + // Check which area is being dragged + var pos = fb.widgetCoords(event); + fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square; + + // Process + fb.mousemove(event); + return false; + }; + + /** + * Mousemove handler + */ + fb.mousemove = function (event) { + // Get coordinates relative to color picker center + var pos = fb.widgetCoords(event); + + // Set new HSL parameters + if (fb.circleDrag) { + var hue = Math.atan2(pos.x, -pos.y) / 6.28; + if (hue < 0) hue += 1; + fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]); + } + else { + var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5)); + var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5)); + fb.setHSL([fb.hsl[0], sat, lum]); + } + return false; + }; + + /** + * Mouseup handler + */ + fb.mouseup = function () { + // Uncapture mouse + $(document).off('mousemove', fb.mousemove); + $(document).off('mouseup', fb.mouseup); + document.dragging = false; + }; + + /** + * Update the markers and styles + */ + fb.updateDisplay = function () { + // Markers + var angle = fb.hsl[0] * 6.28; + $('.h-marker', e).css({ + left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px', + top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px' + }); + + $('.sl-marker', e).css({ + left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px', + top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px' + }); + + // Saturation/Luminance gradient + $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5]))); + + // Linked elements or callback + if (typeof fb.callback == 'object') { + // Set background/foreground color + $(fb.callback).css({ + backgroundColor: fb.color, + color: fb.hsl[2] > 0.5 ? '#000' : '#fff' + }); + + // Change linked value + $(fb.callback).each(function() { + if (this.value && this.value != fb.color) { + this.value = fb.color; + } + }); + } + else if (typeof fb.callback == 'function') { + fb.callback.call(fb, fb.color); + } + }; + + /* Various color utility functions */ + fb.pack = function (rgb) { + var r = Math.round(rgb[0] * 255); + var g = Math.round(rgb[1] * 255); + var b = Math.round(rgb[2] * 255); + return '#' + (r < 16 ? '0' : '') + r.toString(16) + + (g < 16 ? '0' : '') + g.toString(16) + + (b < 16 ? '0' : '') + b.toString(16); + }; + + fb.unpack = function (color) { + if (color.length == 7) { + return [parseInt('0x' + color.substring(1, 3)) / 255, + parseInt('0x' + color.substring(3, 5)) / 255, + parseInt('0x' + color.substring(5, 7)) / 255]; + } + else if (color.length == 4) { + return [parseInt('0x' + color.substring(1, 2)) / 15, + parseInt('0x' + color.substring(2, 3)) / 15, + parseInt('0x' + color.substring(3, 4)) / 15]; + } + }; + + fb.HSLToRGB = function (hsl) { + var m1, m2, r, g, b; + var h = hsl[0], s = hsl[1], l = hsl[2]; + m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s; + m1 = l * 2 - m2; + return [this.hueToRGB(m1, m2, h+0.33333), + this.hueToRGB(m1, m2, h), + this.hueToRGB(m1, m2, h-0.33333)]; + }; + + fb.hueToRGB = function (m1, m2, h) { + h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h); + if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; + if (h * 2 < 1) return m2; + if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6; + return m1; + }; + + fb.RGBToHSL = function (rgb) { + var min, max, delta, h, s, l; + var r = rgb[0], g = rgb[1], b = rgb[2]; + min = Math.min(r, Math.min(g, b)); + max = Math.max(r, Math.max(g, b)); + delta = max - min; + l = (min + max) / 2; + s = 0; + if (l > 0 && l < 1) { + s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l)); + } + h = 0; + if (delta > 0) { + if (max == r && max != g) h += (g - b) / delta; + if (max == g && max != b) h += (2 + (b - r) / delta); + if (max == b && max != r) h += (4 + (r - g) / delta); + h /= 6; + } + return [h, s, l]; + }; + + // Install mousedown handler (the others are set on the document on-demand) + $('*', e).on('mousedown', fb.mousedown); + + // Init color + fb.setColor('#000000'); + + // Set linked elements/callback + if (callback) { + fb.linkTo(callback); + } +}; + +})(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/gallery.js b/wp-admin/js/gallery.js new file mode 100644 index 0000000..761b36d --- /dev/null +++ b/wp-admin/js/gallery.js @@ -0,0 +1,239 @@ +/** + * @output wp-admin/js/gallery.js + */ + +/* global unescape, getUserSetting, setUserSetting, wpgallery, tinymce */ + +jQuery( function($) { + var gallerySortable, gallerySortableInit, sortIt, clearAll, w, desc = false; + + gallerySortableInit = function() { + gallerySortable = $('#media-items').sortable( { + items: 'div.media-item', + placeholder: 'sorthelper', + axis: 'y', + distance: 2, + handle: 'div.filename', + stop: function() { + // When an update has occurred, adjust the order for each item. + var all = $('#media-items').sortable('toArray'), len = all.length; + $.each(all, function(i, id) { + var order = desc ? (len - i) : (1 + i); + $('#' + id + ' .menu_order input').val(order); + }); + } + } ); + }; + + sortIt = function() { + var all = $('.menu_order_input'), len = all.length; + all.each(function(i){ + var order = desc ? (len - i) : (1 + i); + $(this).val(order); + }); + }; + + clearAll = function(c) { + c = c || 0; + $('.menu_order_input').each( function() { + if ( this.value === '0' || c ) { + this.value = ''; + } + }); + }; + + $('#asc').on( 'click', function( e ) { + e.preventDefault(); + desc = false; + sortIt(); + }); + $('#desc').on( 'click', function( e ) { + e.preventDefault(); + desc = true; + sortIt(); + }); + $('#clear').on( 'click', function( e ) { + e.preventDefault(); + clearAll(1); + }); + $('#showall').on( 'click', function( e ) { + e.preventDefault(); + $('#sort-buttons span a').toggle(); + $('a.describe-toggle-on').hide(); + $('a.describe-toggle-off, table.slidetoggle').show(); + $('img.pinkynail').toggle(false); + }); + $('#hideall').on( 'click', function( e ) { + e.preventDefault(); + $('#sort-buttons span a').toggle(); + $('a.describe-toggle-on').show(); + $('a.describe-toggle-off, table.slidetoggle').hide(); + $('img.pinkynail').toggle(true); + }); + + // Initialize sortable. + gallerySortableInit(); + clearAll(); + + if ( $('#media-items>*').length > 1 ) { + w = wpgallery.getWin(); + + $('#save-all, #gallery-settings').show(); + if ( typeof w.tinyMCE !== 'undefined' && w.tinyMCE.activeEditor && ! w.tinyMCE.activeEditor.isHidden() ) { + wpgallery.mcemode = true; + wpgallery.init(); + } else { + $('#insert-gallery').show(); + } + } +}); + +/* gallery settings */ +window.tinymce = null; + +window.wpgallery = { + mcemode : false, + editor : {}, + dom : {}, + is_update : false, + el : {}, + + I : function(e) { + return document.getElementById(e); + }, + + init: function() { + var t = this, li, q, i, it, w = t.getWin(); + + if ( ! t.mcemode ) { + return; + } + + li = ('' + document.location.search).replace(/^\?/, '').split('&'); + q = {}; + for (i=0; i<li.length; i++) { + it = li[i].split('='); + q[unescape(it[0])] = unescape(it[1]); + } + + if ( q.mce_rdomain ) { + document.domain = q.mce_rdomain; + } + + // Find window & API. + window.tinymce = w.tinymce; + window.tinyMCE = w.tinyMCE; + t.editor = tinymce.EditorManager.activeEditor; + + t.setup(); + }, + + getWin : function() { + return window.dialogArguments || opener || parent || top; + }, + + setup : function() { + var t = this, a, ed = t.editor, g, columns, link, order, orderby; + if ( ! t.mcemode ) { + return; + } + + t.el = ed.selection.getNode(); + + if ( t.el.nodeName !== 'IMG' || ! ed.dom.hasClass(t.el, 'wpGallery') ) { + if ( ( g = ed.dom.select('img.wpGallery') ) && g[0] ) { + t.el = g[0]; + } else { + if ( getUserSetting('galfile') === '1' ) { + t.I('linkto-file').checked = 'checked'; + } + if ( getUserSetting('galdesc') === '1' ) { + t.I('order-desc').checked = 'checked'; + } + if ( getUserSetting('galcols') ) { + t.I('columns').value = getUserSetting('galcols'); + } + if ( getUserSetting('galord') ) { + t.I('orderby').value = getUserSetting('galord'); + } + jQuery('#insert-gallery').show(); + return; + } + } + + a = ed.dom.getAttrib(t.el, 'title'); + a = ed.dom.decode(a); + + if ( a ) { + jQuery('#update-gallery').show(); + t.is_update = true; + + columns = a.match(/columns=['"]([0-9]+)['"]/); + link = a.match(/link=['"]([^'"]+)['"]/i); + order = a.match(/order=['"]([^'"]+)['"]/i); + orderby = a.match(/orderby=['"]([^'"]+)['"]/i); + + if ( link && link[1] ) { + t.I('linkto-file').checked = 'checked'; + } + if ( order && order[1] ) { + t.I('order-desc').checked = 'checked'; + } + if ( columns && columns[1] ) { + t.I('columns').value = '' + columns[1]; + } + if ( orderby && orderby[1] ) { + t.I('orderby').value = orderby[1]; + } + } else { + jQuery('#insert-gallery').show(); + } + }, + + update : function() { + var t = this, ed = t.editor, all = '', s; + + if ( ! t.mcemode || ! t.is_update ) { + s = '[gallery' + t.getSettings() + ']'; + t.getWin().send_to_editor(s); + return; + } + + if ( t.el.nodeName !== 'IMG' ) { + return; + } + + all = ed.dom.decode( ed.dom.getAttrib( t.el, 'title' ) ); + all = all.replace(/\s*(order|link|columns|orderby)=['"]([^'"]+)['"]/gi, ''); + all += t.getSettings(); + + ed.dom.setAttrib(t.el, 'title', all); + t.getWin().tb_remove(); + }, + + getSettings : function() { + var I = this.I, s = ''; + + if ( I('linkto-file').checked ) { + s += ' link="file"'; + setUserSetting('galfile', '1'); + } + + if ( I('order-desc').checked ) { + s += ' order="DESC"'; + setUserSetting('galdesc', '1'); + } + + if ( I('columns').value !== 3 ) { + s += ' columns="' + I('columns').value + '"'; + setUserSetting('galcols', I('columns').value); + } + + if ( I('orderby').value !== 'menu_order' ) { + s += ' orderby="' + I('orderby').value + '"'; + setUserSetting('galord', I('orderby').value); + } + + return s; + } +}; diff --git a/wp-admin/js/gallery.min.js b/wp-admin/js/gallery.min.js new file mode 100644 index 0000000..19c1316 --- /dev/null +++ b/wp-admin/js/gallery.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(n){var o=!1,e=function(){n("#media-items").sortable({items:"div.media-item",placeholder:"sorthelper",axis:"y",distance:2,handle:"div.filename",stop:function(){var e=n("#media-items").sortable("toArray"),i=e.length;n.each(e,function(e,t){e=o?i-e:1+e;n("#"+t+" .menu_order input").val(e)})}})},t=function(){var e=n(".menu_order_input"),t=e.length;e.each(function(e){e=o?t-e:1+e;n(this).val(e)})},i=function(e){e=e||0,n(".menu_order_input").each(function(){"0"!==this.value&&!e||(this.value="")})};n("#asc").on("click",function(e){e.preventDefault(),o=!1,t()}),n("#desc").on("click",function(e){e.preventDefault(),o=!0,t()}),n("#clear").on("click",function(e){e.preventDefault(),i(1)}),n("#showall").on("click",function(e){e.preventDefault(),n("#sort-buttons span a").toggle(),n("a.describe-toggle-on").hide(),n("a.describe-toggle-off, table.slidetoggle").show(),n("img.pinkynail").toggle(!1)}),n("#hideall").on("click",function(e){e.preventDefault(),n("#sort-buttons span a").toggle(),n("a.describe-toggle-on").show(),n("a.describe-toggle-off, table.slidetoggle").hide(),n("img.pinkynail").toggle(!0)}),e(),i(),1<n("#media-items>*").length&&(e=wpgallery.getWin(),n("#save-all, #gallery-settings").show(),void 0!==e.tinyMCE&&e.tinyMCE.activeEditor&&!e.tinyMCE.activeEditor.isHidden()?(wpgallery.mcemode=!0,wpgallery.init()):n("#insert-gallery").show())}),window.tinymce=null,window.wpgallery={mcemode:!1,editor:{},dom:{},is_update:!1,el:{},I:function(e){return document.getElementById(e)},init:function(){var e,t,i,n,o=this,l=o.getWin();if(o.mcemode){for(e=(""+document.location.search).replace(/^\?/,"").split("&"),t={},i=0;i<e.length;i++)n=e[i].split("="),t[unescape(n[0])]=unescape(n[1]);t.mce_rdomain&&(document.domain=t.mce_rdomain),window.tinymce=l.tinymce,window.tinyMCE=l.tinyMCE,o.editor=tinymce.EditorManager.activeEditor,o.setup()}},getWin:function(){return window.dialogArguments||opener||parent||top},setup:function(){var e,t,i,n=this,o=n.editor;if(n.mcemode){if(n.el=o.selection.getNode(),"IMG"!==n.el.nodeName||!o.dom.hasClass(n.el,"wpGallery")){if(!(i=o.dom.select("img.wpGallery"))||!i[0])return"1"===getUserSetting("galfile")&&(n.I("linkto-file").checked="checked"),"1"===getUserSetting("galdesc")&&(n.I("order-desc").checked="checked"),getUserSetting("galcols")&&(n.I("columns").value=getUserSetting("galcols")),getUserSetting("galord")&&(n.I("orderby").value=getUserSetting("galord")),void jQuery("#insert-gallery").show();n.el=i[0]}i=o.dom.getAttrib(n.el,"title"),(i=o.dom.decode(i))?(jQuery("#update-gallery").show(),n.is_update=!0,o=i.match(/columns=['"]([0-9]+)['"]/),e=i.match(/link=['"]([^'"]+)['"]/i),t=i.match(/order=['"]([^'"]+)['"]/i),i=i.match(/orderby=['"]([^'"]+)['"]/i),e&&e[1]&&(n.I("linkto-file").checked="checked"),t&&t[1]&&(n.I("order-desc").checked="checked"),o&&o[1]&&(n.I("columns").value=""+o[1]),i&&i[1]&&(n.I("orderby").value=i[1])):jQuery("#insert-gallery").show()}},update:function(){var e=this,t=e.editor,i="";e.mcemode&&e.is_update?"IMG"===e.el.nodeName&&(i=(i=t.dom.decode(t.dom.getAttrib(e.el,"title"))).replace(/\s*(order|link|columns|orderby)=['"]([^'"]+)['"]/gi,""),i+=e.getSettings(),t.dom.setAttrib(e.el,"title",i),e.getWin().tb_remove()):(t="[gallery"+e.getSettings()+"]",e.getWin().send_to_editor(t))},getSettings:function(){var e=this.I,t="";return e("linkto-file").checked&&(t+=' link="file"',setUserSetting("galfile","1")),e("order-desc").checked&&(t+=' order="DESC"',setUserSetting("galdesc","1")),3!==e("columns").value&&(t+=' columns="'+e("columns").value+'"',setUserSetting("galcols",e("columns").value)),"menu_order"!==e("orderby").value&&(t+=' orderby="'+e("orderby").value+'"',setUserSetting("galord",e("orderby").value)),t}};
\ No newline at end of file diff --git a/wp-admin/js/image-edit.js b/wp-admin/js/image-edit.js new file mode 100644 index 0000000..b41e93f --- /dev/null +++ b/wp-admin/js/image-edit.js @@ -0,0 +1,1462 @@ +/** + * 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), + x = t.intval( $('#imgedit-x-' + postid).val() ), + y = t.intval( $('#imgedit-y-' + postid).val() ); + + if ( t.postid !== postid && old.length ) { + t.close(t.postid); + } + + 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.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 ); + }, + + /** + * 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 + * + * @memberof imageEdit + * + * @param {HTMLElement} el The current element. + * + * @return {boolean} Always returns false. + */ + browsePopup : function(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 } }; + } 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 = $( '<img id="image-preview-' + postid + '" alt="" />' ) + .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( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' ); + + 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( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + response.data.error + '</p></div>' ); + + 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( '<div class="notice notice-success" tabindex="-1" role="alert"><p>' + response.data.msg + '</p></div>' ); + + 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( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' ); + } + + 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.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), + selX = $('#imgedit-start-x-' + postid), + selY = $('#imgedit-start-y-' + 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, defRatio; + + if ( e.shiftKey ) { + sel = t.iasapi.getSelection(); + defRatio = t.getSelRatio(postid); + ratio = ( sel && sel.width && sel.height ) ? sel.width + ':' + sel.height : defRatio; + } + + 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; + selW.val( imageEdit.round(c.width / sizer) ); + selH.val( imageEdit.round(c.height / sizer) ); + selX.val( imageEdit.round(c.x1 / sizer) ); + selY.val( imageEdit.round(c.y1 / sizer) ); + } + }); + }, + + /** + * 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; + + 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; + } + + sel = { 'x': c.x1, 'y': c.y1, 'w': c.width, 'h': c.height }; + 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); + }, + + /** + * 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); + }, + + /** + * 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'); + }, + + /** + * 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; + + 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.round( x2 / sizer ) ); + } + + if ( y2 > imgh ) { + y1 = 0; + y2 = imgh; + elY.val( Math.round( y2 / sizer ) ); + } + + ias.setSelection( x1, y1, x2, y2 ); + ias.update(); + this.setCropSelection(postid, 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( '<div class="notice notice-error" tabindex="-1" role="alert"><p>' + errorMessage + '</p></div>' ); + + 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); diff --git a/wp-admin/js/image-edit.min.js b/wp-admin/js/image-edit.min.js new file mode 100644 index 0000000..ee0a364 --- /dev/null +++ b/wp-admin/js/image-edit.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(c){var n=wp.i18n.__,l=window.imageEdit={iasapi:{},hold:{},postid:"",_view:!1,toggleCropTool:function(t,i,e){var a,o,r,s=c("#image-preview-"+t),n=this.iasapi.getSelection();l.toggleControls(e),"false"==("true"===c(e).attr("aria-expanded")?"true":"false")?(this.iasapi.cancelSelection(),l.setDisabled(c(".imgedit-crop-clear"),0)):(l.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():s.innerWidth(),r=c("#imgedit-sel-height-"+t).val()?c("#imgedit-sel-height-"+t).val():s.innerHeight(),isNaN(n.x1)&&(this.setCropSelection(t,{x1:e,y1:a,x2:o,y2:r,width:o,height:r}),n=this.iasapi.getSelection()),0===n.x1&&0===n.y1&&0===n.x2&&0===n.y2?this.iasapi.setSelection(0,0,s.innerWidth(),s.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(),l.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("")):l.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),a=t.intval(c("#imgedit-x-"+e).val()),o=t.intval(c("#imgedit-y-"+e).val());t.postid!==e&&i.length&&t.close(t.postid),t.hold.w=t.hold.ow=a,t.hold.h=t.hold.oh=o,t.hold.xy_ratio=a/o,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&&l.undo(e,i),25===t.which&&t.ctrlKey&&l.redo(e,i)}),c("#imgedit-panel-"+e).on("keypress",'input[type="text"]',function(t){var i=t.keyCode;if(36<i&&i<41&&c(this).trigger("blur"),13===i)return t.preventDefault(),t.stopPropagation(),!1}),c(document).on("image-editor-ui-ready",this.focusManager)},toggleEditor:function(t,i,e){t=c("#imgedit-wait-"+t);i?t.fadeIn("fast"):t.fadeOut("fast",function(){e&&c(document).trigger("image-editor-ui-ready")})},togglePopup:function(t){var i=c(t),t=c(t).attr("aria-controls"),t=c("#"+t);return i.attr("aria-expanded","false"===i.attr("aria-expanded")?"true":"false"),t.toggleClass("imgedit-popup-menu-open").slideToggle("fast").css({"z-index":2e5}),"true"===i.attr("aria-expanded")&&t.find("button").first().trigger("focus"),!1},monitorPopup:function(){var e=document.querySelector(".imgedit-rotate-menu-container"),a=document.querySelector(".imgedit-rotate-menu-container .imgedit-rotate");return setTimeout(function(){var t=document.activeElement,i=e.contains(t);t&&!i&&"true"===a.getAttribute("aria-expanded")&&l.togglePopup(a)},100),!1},browsePopup:function(t){var i=c(t),t=c(t).parent(".imgedit-popup-menu").find("button"),i=t.index(i),e=i-1,i=i+1,a=t.length,a=(e<0&&(e=a-1),i===a&&(i=0),!1);return 40===event.keyCode?a=t.get(i):38===event.keyCode&&(a=t.get(e)),a&&(a.focus(),event.preventDefault()),!1},closePopup:function(t){var t=c(t).parent(".imgedit-popup-menu"),i=t.attr("id");return c('button[aria-controls="'+i+'"]').attr("aria-expanded","false").trigger("focus"),t.toggleClass("imgedit-popup-menu-open").slideToggle("fast"),!1},toggleHelp:function(t){t=c(t);return t.attr("aria-expanded","false"===t.attr("aria-expanded")?"true":"false").parents(".imgedit-group-top").toggleClass("imgedit-help-toggled").find(".imgedit-help").slideToggle("fast"),!1},toggleControls:function(t){var t=c(t),i=c("#"+t.attr("aria-controls"));return t.attr("aria-expanded","false"===t.attr("aria-expanded")?"true":"false"),i.parent(".imgedit-group").toggleClass("imgedit-panel-active"),!1},getTarget:function(t){var i=c("#imgedit-save-target-"+t);return i.length?i.find('input[name="imgedit-target-'+t+'"]:checked').val()||"full":"all"},scaleChanged:function(t,i,e){var a=c("#imgedit-scale-width-"+t),o=c("#imgedit-scale-height-"+t),t=c("#imgedit-scale-warn-"+t),r="",s="",n=c("#imgedit-scale-button");!1!==this.validateNumeric(e)&&(i?(s=""!==a.val()?Math.round(a.val()/this.hold.xy_ratio):"",o.val(s)):(r=""!==o.val()?Math.round(o.val()*this.hold.xy_ratio):"",a.val(r)),s&&s>this.hold.oh||r&&r>this.hold.ow?(t.css("visibility","visible"),n.prop("disabled",!0)):(t.css("visibility","hidden"),n.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(),s=[];if(""===r)return"";if(r=JSON.parse(r),0<(e=this.intval(c("#imgedit-undone-"+t).val())))for(;0<e;)r.pop(),e--;if(i){if(!r.length)return this.hold.w=this.hold.ow,this.hold.h=this.hold.oh,"";(t=(t=r[r.length-1]).c||t.r||t.f||!1)&&(this.hold.w=t.fw,this.hold.h=t.fh)}for(a in r)(o=r[a]).hasOwnProperty("c")?s[a]={c:{x:o.c.x,y:o.c.y,w:o.c.w,h:o.c.h}}:o.hasOwnProperty("r")?s[a]={r:o.r.r}:o.hasOwnProperty("f")&&(s[a]={f:o.f.f});return JSON.stringify(s)},refreshEditor:function(o,t,r){var s,i=this;i.toggleEditor(o,1),t={action:"imgedit-preview",_ajax_nonce:t,postid:o,history:i.filterHistory(o,1),rand:i.intval(1e6*Math.random())},s=c('<img id="image-preview-'+o+'" alt="" />').on("load",{history:t.history},function(t){var i=c("#imgedit-crop-"+o),e=l,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(s),t=Math.max(e.hold.w,e.hold.h),a=Math.max(c(s).width(),c(s).height()),e.hold.sizer=a<t?a/t:1,e.initCrop(o,s,i),null!=r&&r(),c("#imgedit-history-"+o).val()&&"0"===c("#imgedit-undone-"+o).val()?c("button.imgedit-submit-btn","#imgedit-panel-"+o).prop("disabled",!1):c("button.imgedit-submit-btn","#imgedit-panel-"+o).prop("disabled",!0),n("Image updated."));e.toggleEditor(o,0),wp.a11y.speak(a,"assertive")}).on("error",function(){var t=n("Could not load the preview image. Please reload the page and try again.");c("#imgedit-crop-"+o).empty().append('<div class="notice notice-error" tabindex="-1" role="alert"><p>'+t+"</p></div>"),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,s,n=this;if(n.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=n.intval(a.val()),s=n.intval(o.val()),r<1)return a.trigger("focus"),!1;if(s<1)return o.trigger("focus"),!1;if(r===n.hold.ow||s===n.hold.oh)return!1;t.do="scale",t.fwidth=r,t.fheight=s}else{if("restore"!==e)return!1;t.do="restore"}n.toggleEditor(i,1),c.post(ajaxurl,t,function(t){c("#image-editor-"+i).empty().append(t.data.html),n.toggleEditor(i,0,!0),n._view&&n._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('<div class="notice notice-error" tabindex="-1" role="alert"><p>'+t.data.error+"</p></div>"),l.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('<div class="notice notice-success" tabindex="-1" role="alert"><p>'+t.data.msg+"</p></div>"),wp.a11y.speak(t.data.msg)),o._view?o._view.save():l.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),s=r.siblings(".spinner");if(!r.hasClass("button-activated"))return s.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=n("Could not load the preview image."),a.html('<div class="notice notice-error" tabindex="-1" role="alert"><p>'+i+"</p></div>")),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"),s.removeClass("is-active")}),l.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.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(a,t,i){var o=this,r=c("#imgedit-sel-width-"+a),s=c("#imgedit-sel-height-"+a),n=c("#imgedit-start-x-"+a),d=c("#imgedit-start-y-"+a),t=c(t);t.data("imgAreaSelect")||(o.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,e=!1;t.shiftKey&&(t=o.iasapi.getSelection(),i=o.getSelRatio(a),e=t&&t.width&&t.height?t.width+":"+t.height:i),o.iasapi.setOptions({aspectRatio:e})})},onSelectStart:function(){l.setDisabled(c("#imgedit-crop-sel-"+a),1),l.setDisabled(c(".imgedit-crop-clear"),1),l.setDisabled(c(".imgedit-crop-apply"),1)},onSelectEnd:function(t,i){l.setCropSelection(a,i),c("#imgedit-crop > *").is(":visible")||l.toggleControls(c(".imgedit-crop.button"))},onSelectChange:function(t,i){var e=l.hold.sizer;r.val(l.round(i.width/e)),s.val(l.round(i.height/e)),n.val(l.round(i.x1/e)),d.val(l.round(i.y1/e))}}))},setCropSelection:function(t,i){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;i={x:i.x1,y:i.y1,w:i.width,h:i.height},this.setDisabled(c(".imgedit-crop","#imgedit-panel-"+t),1),c("#imgedit-selection-"+t).val(JSON.stringify(i))},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())<i.length&&!confirm(c("#imgedit-leaving-"+t).text())},addStep:function(t,i,e){for(var a=this,o=c("#imgedit-history-"+i),r=""!==o.val()?JSON.parse(o.val()):[],s=c("#imgedit-undone-"+i),n=a.intval(s.val());0<n;)r.pop(),n--;s.val(0),r.push(t),o.val(JSON.stringify(r)),a.refreshEditor(i,e,function(){a.setDisabled(c("#image-undo-"+i),!0),a.setDisabled(c("#image-redo-"+i),!1)})},rotate:function(t,i,e,a){if(c(a).hasClass("disabled"))return!1;this.closePopup(a),this.addStep({r:{r:t,fw:this.hold.h,fh:this.hold.w}},i,e)},flip:function(t,i,e,a){if(c(a).hasClass("disabled"))return!1;this.closePopup(a),this.addStep({f:{f:t,fw:this.hold.w,fh:this.hold.h}},i,e)},crop:function(t,i,e){var a=c("#imgedit-selection-"+t).val(),o=this.intval(c("#imgedit-sel-width-"+t).val()),r=this.intval(c("#imgedit-sel-height-"+t).val());if(c(e).hasClass("disabled")||""===a)return!1;0<(a=JSON.parse(a)).w&&0<a.h&&0<o&&0<r&&(a.fw=o,a.fh=r,this.addStep({c:a},t,i)),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")},undo:function(i,t){var e=this,a=c("#image-undo-"+i),o=c("#imgedit-undone-"+i),r=e.intval(o.val())+1;a.hasClass("disabled")||(o.val(r),e.refreshEditor(i,t,function(){var t=c("#imgedit-history-"+i),t=""!==t.val()?JSON.parse(t.val()):[];e.setDisabled(c("#image-redo-"+i),!0),e.setDisabled(a,r<t.length),t.length===r&&c("#image-redo-"+i).trigger("focus")}))},redo:function(t,i){var e=this,a=c("#image-redo-"+t),o=c("#imgedit-undone-"+t),r=e.intval(o.val())-1;a.hasClass("disabled")||(o.val(r),e.refreshEditor(t,i,function(){e.setDisabled(c("#image-undo-"+t),!0),e.setDisabled(a,0<r),0==r&&c("#image-undo-"+t).trigger("focus")}))},setNumSelection:function(t,i){var e=c("#imgedit-sel-width-"+t),a=c("#imgedit-sel-height-"+t),o=c("#imgedit-start-x-"+t),r=c("#imgedit-start-y-"+t),o=this.intval(o.val()),r=this.intval(r.val()),s=this.intval(e.val()),n=this.intval(a.val()),d=c("#image-preview-"+t),l=d.height(),d=d.width(),h=this.hold.sizer,g=this.iasapi;if(!1!==this.validateNumeric(i))return s<1?(e.val(""),!1):n<1?(a.val(""),!1):void((s&&n||o&&r)&&(i=g.getSelection())&&(s=i.x1+Math.round(s*h),n=i.y1+Math.round(n*h),o=o===i.x1?i.x1:Math.round(o*h),i=r===i.y1?i.y1:Math.round(r*h),d<s&&(o=0,s=d,e.val(Math.round(s/h))),l<n&&(i=0,n=l,a.val(Math.round(n/h))),g.setSelection(o,i,s,n),g.update(),this.setCropSelection(t,g.getSelection())))},round:function(t){var i;return t=Math.round(t),.6<this.hold.sizer?t:"1"===(i=t.toString().slice(-1))?t-1:"9"===i?t+1:t},setRatioSelection:function(t,i,e){var a=this.intval(c("#imgedit-crop-width-"+t).val()),o=this.intval(c("#imgedit-crop-height-"+t).val()),r=c("#image-preview-"+t).height();!1===this.validateNumeric(e)?this.iasapi.setOptions({aspectRatio:null}):a&&o&&(this.iasapi.setOptions({aspectRatio:a+":"+o}),e=this.iasapi.getSelection(!0))&&(r<(a=Math.ceil(e.y1+(e.x2-e.x1)/(a/o)))?(a=r,o=n("Selected crop ratio exceeds the boundaries of the image. Try a different ratio."),c("#imgedit-crop-"+t).prepend('<div class="notice notice-error" tabindex="-1" role="alert"><p>'+o+"</p></div>"),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);
\ No newline at end of file diff --git a/wp-admin/js/inline-edit-post.js b/wp-admin/js/inline-edit-post.js new file mode 100644 index 0000000..e7d4496 --- /dev/null +++ b/wp-admin/js/inline-edit-post.js @@ -0,0 +1,604 @@ +/** + * 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 ); + }); + + $('#bulk-edit').find('fieldset:first').after( + $('#inline-edit fieldset.inline-edit-categories').clone() + ).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; + + 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; + 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('<tr class="hidden"></tr>'); + $('#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 += '<li class="ntdelitem"><button type="button" id="_' + id + '" class="button-link ntdelbutton"><span class="screen-reader-text">' + buttonVisuallyHiddenText + '</span></button><span class="ntdeltitle" aria-hidden="true">' + theTitle + '</span></li>'; + } + }); + + // 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( '<ul id="bulk-titles-list" role="list">' + te + '</ul>' ); + + /** + * 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' ); + + $( '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('<tr class="hidden"></tr>'); + + // 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('<option value="' + $('.post_author', rowData).text() + '">' + $('#post-' + id + ' .author').text() + '</option>'); + } + 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( '<tr' ) ) { + $(inlineEditPost.what+id).siblings('tr.hidden').addBack().remove(); + $('#edit-'+id).before(r).remove(); + $( inlineEditPost.what + id ).hide().fadeIn( 400, function() { + // Move focus back to the Quick Edit button. $( this ) is the row being animated. + $( this ).find( '.editinline' ) + .attr( 'aria-expanded', 'false' ) + .trigger( 'focus' ); + wp.a11y.speak( wp.i18n.__( 'Changes saved.' ) ); + }); + } else { + r = r.replace( /<.[^<>]*?>/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 15 seconds. + if ( typeof wp !== 'undefined' && wp.heartbeat ) { + wp.heartbeat.interval( 15 ); + } +}).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 = $( '<img />', { + '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 ); diff --git a/wp-admin/js/inline-edit-post.min.js b/wp-admin/js/inline-edit-post.min.js new file mode 100644 index 0000000..6956f57 --- /dev/null +++ b/wp-admin/js/inline-edit-post.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},function(u,h){window.inlineEditPost={init:function(){var i=this,t=u("#inline-edit"),e=u("#bulk-edit");i.type=u("table.widefat").hasClass("pages")?"page":"post",i.what="#post-",t.on("keyup",function(t){if(27===t.which)return inlineEditPost.revert()}),e.on("keyup",function(t){if(27===t.which)return inlineEditPost.revert()}),u(".cancel",t).on("click",function(){return inlineEditPost.revert()}),u(".save",t).on("click",function(){return inlineEditPost.save(this)}),u("td",t).on("keydown",function(t){if(13===t.which&&!u(t.target).hasClass("cancel"))return inlineEditPost.save(this)}),u(".cancel",e).on("click",function(){return inlineEditPost.revert()}),u('#inline-edit .inline-edit-private input[value="private"]').on("click",function(){var t=u("input.inline-edit-password-input");u(this).prop("checked")?t.val("").prop("disabled",!0):t.prop("disabled",!1)}),u("#the-list").on("click",".editinline",function(){u(this).attr("aria-expanded","true"),inlineEditPost.edit(this)}),u("#bulk-edit").find("fieldset:first").after(u("#inline-edit fieldset.inline-edit-categories").clone()).siblings("fieldset:last").prepend(u("#inline-edit .inline-edit-tags-wrap").clone()),u('select[name="_status"] option[value="future"]',e).remove(),u("#doaction").on("click",function(t){var e;i.whichBulkButtonId=u(this).attr("id"),e=i.whichBulkButtonId.substr(2),"edit"===u('select[name="'+e+'"]').val()?(t.preventDefault(),i.setBulk()):0<u("form#posts-filter tr.inline-editor").length&&i.revert()})},toggle:function(t){var e=this;"none"===u(e.what+e.getId(t)).css("display")?e.revert():e.edit(t)},setBulk:function(){var n="",t=this.type,a=!0;if(this.revert(),u("#bulk-edit td").attr("colspan",u("th:visible, td:visible",".widefat:first thead").length),u("table.widefat tbody").prepend(u("#bulk-edit")).prepend('<tr class="hidden"></tr>'),u("#bulk-edit").addClass("inline-editor").show(),u('tbody th.check-column input[type="checkbox"]').each(function(){var t,e,i;u(this).prop("checked")&&(a=!1,t=u(this).val(),e=u("#inline_"+t+" .post_title").html()||h.i18n.__("(no title)"),i=h.i18n.sprintf(h.i18n.__("Remove “%s” from Bulk Edit"),e),n+='<li class="ntdelitem"><button type="button" id="_'+t+'" class="button-link ntdelbutton"><span class="screen-reader-text">'+i+'</span></button><span class="ntdeltitle" aria-hidden="true">'+e+"</span></li>")}),a)return this.revert();u("#bulk-titles").html('<ul id="bulk-titles-list" role="list">'+n+"</ul>"),u("#bulk-titles .ntdelbutton").click(function(){var t=u(this),e=t.attr("id").substr(1),i=t.parent().prev().children(".ntdelbutton"),t=t.parent().next().children(".ntdelbutton");u('table.widefat input[value="'+e+'"]').prop("checked",!1),u("#_"+e).parent().remove(),h.a11y.speak(h.i18n.__("Item removed."),"assertive"),t.length?t.focus():i.length?i.focus():(u("#bulk-titles-list").remove(),inlineEditPost.revert(),h.a11y.speak(h.i18n.__("All selected items have been removed. Select new items to use Bulk Actions.")))}),"post"===t&&u("tr.inline-editor textarea[data-wp-taxonomy]").each(function(t,e){u(e).autocomplete("instance")||u(e).wpTagsSuggest()}),u("#bulk-edit .inline-edit-wrapper").attr("tabindex","-1").focus(),u("html, body").animate({scrollTop:0},"fast")},edit:function(n){var t,a,e,i,s,r,l,o,d=this,p=!0;for(d.revert(),"object"==typeof n&&(n=d.getId(n)),t=["post_title","post_name","post_author","_status","jj","mm","aa","hh","mn","ss","post_password","post_format","menu_order","page_template"],"page"===d.type&&t.push("post_parent"),a=u("#inline-edit").clone(!0),u("td",a).attr("colspan",u("th:visible, td:visible",".widefat:first thead").length),u("td",a).find("#quick-edit-legend").removeAttr("id"),u("td",a).find('p[id^="quick-edit-"]').removeAttr("id"),u(d.what+n).removeClass("is-expanded").hide().after(a).after('<tr class="hidden"></tr>'),e=u("#inline_"+n),u(':input[name="post_author"] option[value="'+u(".post_author",e).text()+'"]',a).val()||u(':input[name="post_author"]',a).prepend('<option value="'+u(".post_author",e).text()+'">'+u("#post-"+n+" .author").text()+"</option>"),1===u(':input[name="post_author"] option',a).length&&u("label.inline-edit-author",a).hide(),l=0;l<t.length;l++)(o=u("."+t[l],e)).find("img").replaceWith(function(){return this.alt}),o=o.text(),u(':input[name="'+t[l]+'"]',a).val(o);"open"===u(".comment_status",e).text()&&u('input[name="comment_status"]',a).prop("checked",!0),"open"===u(".ping_status",e).text()&&u('input[name="ping_status"]',a).prop("checked",!0),"sticky"===u(".sticky",e).text()&&u('input[name="sticky"]',a).prop("checked",!0),u(".post_category",e).each(function(){var t,e=u(this).text();e&&(t=u(this).attr("id").replace("_"+n,""),u("ul."+t+"-checklist :checkbox",a).val(e.split(",")))}),u(".tags_input",e).each(function(){var t=u(this),e=u(this).attr("id").replace("_"+n,""),e=u("textarea.tax_input_"+e,a),i=h.i18n._x(",","tag delimiter").trim();e.length&&(t.find("img").replaceWith(function(){return this.alt}),(t=t.text())&&(","!==i&&(t=t.replace(/,/g,i)),e.val(t)),e.wpTagsSuggest())});var c,d=u(':input[name="aa"]').val()+"-"+u(':input[name="mm"]').val()+"-"+u(':input[name="jj"]').val(),d=(d+=" "+u(':input[name="hh"]').val()+":"+u(':input[name="mn"]').val()+":"+u(':input[name="ss"]').val(),new Date(d));if(("future"!==(c=u("._status",e).text())&&Date.now()>d?u('select[name="_status"] option[value="future"]',a):u('select[name="_status"] option[value="publish"]',a)).remove(),d=u(".inline-edit-password-input").prop("disabled",!1),"private"===c&&(u('input[name="keep_private"]',a).prop("checked",!0),d.val("").prop("disabled",!0)),0<(i=u('select[name="post_parent"] option[value="'+n+'"]',a)).length){for(s=i[0].className.split("-")[1],r=i;p&&0!==(r=r.next("option")).length;)r[0].className.split("-")[1]<=s?p=!1:(r.remove(),r=i);i.remove()}return u(a).attr("id","edit-"+n).addClass("inline-editor").show(),u(".ptitle",a).trigger("focus"),!1},save:function(n){var t=u(".post_status_page").val()||"";return"object"==typeof n&&(n=this.getId(n)),u("table.widefat .spinner").addClass("is-active"),t={action:"inline-save",post_type:typenow,post_ID:n,edit_date:"true",post_status:t},t=u("#edit-"+n).find(":input").serialize()+"&"+u.param(t),u.post(ajaxurl,t,function(t){var e=u("#edit-"+n+" .inline-edit-save .notice-error"),i=e.find(".error");u("table.widefat .spinner").removeClass("is-active"),t?-1!==t.indexOf("<tr")?(u(inlineEditPost.what+n).siblings("tr.hidden").addBack().remove(),u("#edit-"+n).before(t).remove(),u(inlineEditPost.what+n).hide().fadeIn(400,function(){u(this).find(".editinline").attr("aria-expanded","false").trigger("focus"),h.a11y.speak(h.i18n.__("Changes saved."))})):(t=t.replace(/<.[^<>]*?>/g,""),e.removeClass("hidden"),i.html(t),h.a11y.speak(i.text())):(e.removeClass("hidden"),i.text(h.i18n.__("Error while saving the changes.")),h.a11y.speak(h.i18n.__("Error while saving the changes.")))},"html"),!1},revert:function(){var t=u(".widefat"),e=u(".inline-editor",t).attr("id");return e&&(u(".spinner",t).removeClass("is-active"),("bulk-edit"===e?(u("#bulk-edit",t).removeClass("inline-editor").hide().siblings(".hidden").remove(),u("#bulk-titles").empty(),u("#inlineedit").append(u("#bulk-edit")),u("#"+inlineEditPost.whichBulkButtonId)):(u("#"+e).siblings("tr.hidden").addBack().remove(),e=e.substr(e.lastIndexOf("-")+1),u(this.what+e).show().find(".editinline").attr("aria-expanded","false"))).trigger("focus")),!1},getId:function(t){t=u(t).closest("tr").attr("id").split("-");return t[t.length-1]}},u(function(){inlineEditPost.init()}),u(function(){void 0!==h&&h.heartbeat&&h.heartbeat.interval(15)}).on("heartbeat-tick.wp-check-locked-posts",function(t,e){var n=e["wp-check-locked-posts"]||{};u("#the-list tr").each(function(t,e){var i=e.id,e=u(e);n.hasOwnProperty(i)?e.hasClass("wp-locked")||(i=n[i],e.find(".column-title .locked-text").text(i.text),e.find(".check-column checkbox").prop("checked",!1),i.avatar_src&&(i=u("<img />",{class:"avatar avatar-18 photo",width:18,height:18,alt:"",src:i.avatar_src,srcset:i.avatar_src_2x?i.avatar_src_2x+" 2x":void 0}),e.find(".column-title .locked-avatar").empty().append(i)),e.addClass("wp-locked")):e.hasClass("wp-locked")&&e.removeClass("wp-locked").find(".locked-info span").empty()})}).on("heartbeat-send.wp-check-locked-posts",function(t,e){var i=[];u("#the-list tr").each(function(t,e){e.id&&i.push(e.id)}),i.length&&(e["wp-check-locked-posts"]=i)})}(jQuery,window.wp);
\ No newline at end of file diff --git a/wp-admin/js/inline-edit-tax.js b/wp-admin/js/inline-edit-tax.js new file mode 100644 index 0000000..86e3498 --- /dev/null +++ b/wp-admin/js/inline-edit-tax.js @@ -0,0 +1,294 @@ +/** + * 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('<tr class="hidden"></tr>'); + + 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( '<tr' ) ) { + $(inlineEditTax.what+id).siblings('tr.hidden').addBack().remove(); + new_id = $(r).attr('id'); + + $('#edit-'+id).before(r).remove(); + + if ( new_id ) { + option_value = new_id.replace( inlineEditTax.type + '-', '' ); + row = $( '#' + new_id ); + } else { + option_value = id; + row = $( inlineEditTax.what + id ); + } + + // Update the value in the Parent dropdown. + $( '#parent' ).find( 'option[value=' + option_value + ']' ).text( row.find( '.row-title' ).text() ); + + row.hide().fadeIn( 400, function() { + // Move focus back to the Quick Edit button. + row.find( '.editinline' ) + .attr( 'aria-expanded', 'false' ) + .trigger( 'focus' ); + wp.a11y.speak( wp.i18n.__( 'Changes saved.' ) ); + }); + + } else { + $errorNotice.removeClass( 'hidden' ); + $error.html( r ); + /* + * Some error strings may contain HTML entities (e.g. `“`), let's use + * the HTML element's text. + */ + 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.' ) ); + } + } + ); + + // Prevent submitting the form when pressing Enter on a focused field. + return false; + }, + + /** + * Closes the quick edit form. + * + * @since 2.7.0 + * + * @this inlineEditTax + * @memberof inlineEditTax + * @return {void} + */ + revert : function() { + var id = $('table.widefat tr.inline-editor').attr('id'); + + if ( id ) { + $( 'table.widefat .spinner' ).removeClass( 'is-active' ); + $('#'+id).siblings('tr.hidden').addBack().remove(); + id = id.substr( id.lastIndexOf('-') + 1 ); + + // Show the taxonomy row and move focus back to the Quick Edit button. + $( this.what + id ).show().find( '.editinline' ) + .attr( 'aria-expanded', 'false' ) + .trigger( 'focus' ); + } + }, + + /** + * Retrieves the ID of the term of the element inside the table row. + * + * @since 2.7.0 + * + * @memberof inlineEditTax + * + * @param {HTMLElement} o An element within the table row or the table row itself. + * @return {string} The ID of the term based on the element. + */ + getId : function(o) { + var id = o.tagName === 'TR' ? o.id : $(o).parents('tr').attr('id'), parts = id.split('-'); + + return parts[parts.length - 1]; + } +}; + +$( function() { inlineEditTax.init(); } ); + +})( jQuery, window.wp ); diff --git a/wp-admin/js/inline-edit-tax.min.js b/wp-admin/js/inline-edit-tax.min.js new file mode 100644 index 0000000..16c35cd --- /dev/null +++ b/wp-admin/js/inline-edit-tax.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},function(s,l){window.inlineEditTax={init:function(){var t=this,i=s("#inline-edit");t.type=s("#the-list").attr("data-wp-lists").substr(5),t.what="#"+t.type+"-",s("#the-list").on("click",".editinline",function(){s(this).attr("aria-expanded","true"),inlineEditTax.edit(this)}),i.on("keyup",function(t){if(27===t.which)return inlineEditTax.revert()}),s(".cancel",i).on("click",function(){return inlineEditTax.revert()}),s(".save",i).on("click",function(){return inlineEditTax.save(this)}),s("input, select",i).on("keydown",function(t){if(13===t.which)return inlineEditTax.save(this)}),s('#posts-filter input[type="submit"]').on("mousedown",function(){t.revert()})},toggle:function(t){var i=this;"none"===s(i.what+i.getId(t)).css("display")?i.revert():i.edit(t)},edit:function(t){var i,e,n=this;return n.revert(),"object"==typeof t&&(t=n.getId(t)),i=s("#inline-edit").clone(!0),e=s("#inline_"+t),s("td",i).attr("colspan",s("th:visible, td:visible",".wp-list-table.widefat:first thead").length),s(n.what+t).hide().after(i).after('<tr class="hidden"></tr>'),(n=s(".name",e)).find("img").replaceWith(function(){return this.alt}),n=n.text(),s(':input[name="name"]',i).val(n),(n=s(".slug",e)).find("img").replaceWith(function(){return this.alt}),n=n.text(),s(':input[name="slug"]',i).val(n),s(i).attr("id","edit-"+t).addClass("inline-editor").show(),s(".ptitle",i).eq(0).trigger("focus"),!1},save:function(d){var t=s('input[name="taxonomy"]').val()||"";return"object"==typeof d&&(d=this.getId(d)),s("table.widefat .spinner").addClass("is-active"),t={action:"inline-save-tax",tax_type:this.type,tax_ID:d,taxonomy:t},t=s("#edit-"+d).find(":input").serialize()+"&"+s.param(t),s.post(ajaxurl,t,function(t){var i,e,n,a=s("#edit-"+d+" .inline-edit-save .notice-error"),r=a.find(".error");s("table.widefat .spinner").removeClass("is-active"),t?-1!==t.indexOf("<tr")?(s(inlineEditTax.what+d).siblings("tr.hidden").addBack().remove(),e=s(t).attr("id"),s("#edit-"+d).before(t).remove(),i=e?(n=e.replace(inlineEditTax.type+"-",""),s("#"+e)):(n=d,s(inlineEditTax.what+d)),s("#parent").find("option[value="+n+"]").text(i.find(".row-title").text()),i.hide().fadeIn(400,function(){i.find(".editinline").attr("aria-expanded","false").trigger("focus"),l.a11y.speak(l.i18n.__("Changes saved."))})):(a.removeClass("hidden"),r.html(t),l.a11y.speak(r.text())):(a.removeClass("hidden"),r.text(l.i18n.__("Error while saving the changes.")),l.a11y.speak(l.i18n.__("Error while saving the changes.")))}),!1},revert:function(){var t=s("table.widefat tr.inline-editor").attr("id");t&&(s("table.widefat .spinner").removeClass("is-active"),s("#"+t).siblings("tr.hidden").addBack().remove(),t=t.substr(t.lastIndexOf("-")+1),s(this.what+t).show().find(".editinline").attr("aria-expanded","false").trigger("focus"))},getId:function(t){t=("TR"===t.tagName?t.id:s(t).parents("tr").attr("id")).split("-");return t[t.length-1]}},s(function(){inlineEditTax.init()})}(jQuery,window.wp);
\ No newline at end of file diff --git a/wp-admin/js/iris.min.js b/wp-admin/js/iris.min.js new file mode 100644 index 0000000..b6aabc9 --- /dev/null +++ b/wp-admin/js/iris.min.js @@ -0,0 +1,5 @@ +/*! This file is auto-generated */ +/*! Iris Color Picker - v1.1.1 - 2021-10-05 +* https://github.com/Automattic/Iris +* Copyright (c) 2021 Matt Wiebe; Licensed GPLv2 */ +!function(a,b){function c(){var b,c,d="backgroundImage";j?k="filter":(b=a('<div id="iris-gradtest" />'),c="linear-gradient(top,#fff,#000)",a.each(l,function(a,e){if(b.css(d,e+c),b.css(d).match("gradient"))return k=a,!1}),!1===k&&(b.css("background","-webkit-gradient(linear,0% 0%,0% 100%,from(#fff),to(#000))"),b.css(d).match("gradient")&&(k="webkit")),b.remove())}function d(a,b){return a="top"===a?"top":"left",b=Array.isArray(b)?b:Array.prototype.slice.call(arguments,1),"webkit"===k?f(a,b):l[k]+"linear-gradient("+a+", "+b.join(", ")+")"}function e(b,c){var d,e,f,h,i,j,k,l,m;b="top"===b?"top":"left",c=Array.isArray(c)?c:Array.prototype.slice.call(arguments,1),d="top"===b?0:1,e=a(this),f=c.length-1,h="filter",i=1===d?"left":"top",j=1===d?"right":"bottom",k=1===d?"height":"width",l='<div class="iris-ie-gradient-shim" style="position:absolute;'+k+":100%;"+i+":%start%;"+j+":%end%;"+h+':%filter%;" data-color:"%color%"></div>',m="","static"===e.css("position")&&e.css({position:"relative"}),c=g(c),a.each(c,function(a,b){var e,g,h;if(a===f)return!1;e=c[a+1],b.stop!==e.stop&&(g=100-parseFloat(e.stop)+"%",b.octoHex=new Color(b.color).toIEOctoHex(),e.octoHex=new Color(e.color).toIEOctoHex(),h="progid:DXImageTransform.Microsoft.Gradient(GradientType="+d+", StartColorStr='"+b.octoHex+"', EndColorStr='"+e.octoHex+"')",m+=l.replace("%start%",b.stop).replace("%end%",g).replace("%filter%",h))}),e.find(".iris-ie-gradient-shim").remove(),a(m).prependTo(e)}function f(b,c){var d=[];return b="top"===b?"0% 0%,0% 100%,":"0% 100%,100% 100%,",c=g(c),a.each(c,function(a,b){d.push("color-stop("+parseFloat(b.stop)/100+", "+b.color+")")}),"-webkit-gradient(linear,"+b+d.join(",")+")"}function g(b){var c=[],d=[],e=[],f=b.length-1;return a.each(b,function(a,b){var e=b,f=!1,g=b.match(/1?[0-9]{1,2}%$/);g&&(e=b.replace(/\s?1?[0-9]{1,2}%$/,""),f=g.shift()),c.push(e),d.push(f)}),!1===d[0]&&(d[0]="0%"),!1===d[f]&&(d[f]="100%"),d=h(d),a.each(d,function(a){e[a]={color:c[a],stop:d[a]}}),e}function h(b){var c,d,e,f,g=0,i=b.length-1,j=0,k=!1;if(b.length<=2||a.inArray(!1,b)<0)return b;for(;j<b.length-1;)k||!1!==b[j]?k&&!1!==b[j]&&(i=j,j=b.length):(g=j-1,k=!0),j++;for(d=i-g,f=parseInt(b[g].replace("%"),10),c=(parseFloat(b[i].replace("%"))-f)/d,j=g+1,e=1;j<i;)b[j]=f+e*c+"%",e++,j++;return h(b)}var i,j,k,l,m,n,o,p,q;if(i='<div class="iris-picker"><div class="iris-picker-inner"><div class="iris-square"><a class="iris-square-value" href="#"><span class="iris-square-handle ui-slider-handle"></span></a><div class="iris-square-inner iris-square-horiz"></div><div class="iris-square-inner iris-square-vert"></div></div><div class="iris-slider iris-strip"><div class="iris-slider-offset"></div></div></div></div>',m='.iris-picker{display:block;position:relative}.iris-picker,.iris-picker *{-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input+.iris-picker{margin-top:4px}.iris-error{background-color:#ffafaf}.iris-border{border-radius:3px;border:1px solid #aaa;width:200px;background-color:#fff}.iris-picker-inner{position:absolute;top:0;right:0;left:0;bottom:0}.iris-border .iris-picker-inner{top:10px;right:10px;left:10px;bottom:10px}.iris-picker .iris-square-inner{position:absolute;left:0;right:0;top:0;bottom:0}.iris-picker .iris-square,.iris-picker .iris-slider,.iris-picker .iris-square-inner,.iris-picker .iris-palette{border-radius:3px;box-shadow:inset 0 0 5px rgba(0,0,0,.4);height:100%;width:12.5%;float:left;margin-right:5%}.iris-only-strip .iris-slider{width:100%}.iris-picker .iris-square{width:76%;margin-right:10%;position:relative}.iris-only-strip .iris-square{display:none}.iris-picker .iris-square-inner{width:auto;margin:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-square-inner,.iris-ie-9 .iris-palette{box-shadow:none;border-radius:0}.iris-ie-9 .iris-square,.iris-ie-9 .iris-slider,.iris-ie-9 .iris-palette{outline:1px solid rgba(0,0,0,.1)}.iris-ie-lt9 .iris-square,.iris-ie-lt9 .iris-slider,.iris-ie-lt9 .iris-square-inner,.iris-ie-lt9 .iris-palette{outline:1px solid #aaa}.iris-ie-lt9 .iris-square .ui-slider-handle{outline:1px solid #aaa;background-color:#fff;-ms-filter:"alpha(Opacity=30)"}.iris-ie-lt9 .iris-square .iris-square-handle{background:0 0;border:3px solid #fff;-ms-filter:"alpha(Opacity=50)"}.iris-picker .iris-strip{margin-right:0;position:relative}.iris-picker .iris-strip .ui-slider-handle{position:absolute;background:0 0;margin:0;right:-3px;left:-3px;border:4px solid #aaa;border-width:4px 3px;width:auto;height:6px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2);opacity:.9;z-index:5;cursor:ns-resize}.iris-strip-horiz .iris-strip .ui-slider-handle{right:auto;left:auto;bottom:-3px;top:-3px;height:auto;width:6px;cursor:ew-resize}.iris-strip .ui-slider-handle:before{content:" ";position:absolute;left:-2px;right:-2px;top:-3px;bottom:-3px;border:2px solid #fff;border-radius:3px}.iris-picker .iris-slider-offset{position:absolute;top:11px;left:0;right:0;bottom:-3px;width:auto;height:auto;background:transparent;border:0;border-radius:0}.iris-strip-horiz .iris-slider-offset{top:0;bottom:0;right:11px;left:-3px}.iris-picker .iris-square-handle{background:transparent;border:5px solid #aaa;border-radius:50%;border-color:rgba(128,128,128,.5);box-shadow:none;width:12px;height:12px;position:absolute;left:-10px;top:-10px;cursor:move;opacity:1;z-index:10}.iris-picker .ui-state-focus .iris-square-handle{opacity:.8}.iris-picker .iris-square-handle:hover{border-color:#999}.iris-picker .iris-square-value:focus .iris-square-handle{box-shadow:0 0 2px rgba(0,0,0,.75);opacity:.8}.iris-picker .iris-square-handle:hover::after{border-color:#fff}.iris-picker .iris-square-handle::after{position:absolute;bottom:-4px;right:-4px;left:-4px;top:-4px;border:3px solid #f9f9f9;border-color:rgba(255,255,255,.8);border-radius:50%;content:" "}.iris-picker .iris-square-value{width:8px;height:8px;position:absolute}.iris-ie-lt9 .iris-square-value,.iris-mozilla .iris-square-value{width:1px;height:1px}.iris-palette-container{position:absolute;bottom:0;left:0;margin:0;padding:0}.iris-border .iris-palette-container{left:10px;bottom:10px}.iris-picker .iris-palette{margin:0;cursor:pointer}.iris-square-handle,.ui-slider-handle{border:0;outline:0}',o=navigator.userAgent.toLowerCase(),p="Microsoft Internet Explorer"===navigator.appName,q=p?parseFloat(o.match(/msie ([0-9]{1,}[\.0-9]{0,})/)[1]):0,j=p&&q<10,k=!1,l=["-moz-","-webkit-","-o-","-ms-"],j&&q<=7)return a.fn.iris=a.noop,void(a.support.iris=!1);a.support.iris=!0,a.fn.gradient=function(){var b=arguments;return this.each(function(){j?e.apply(this,b):a(this).css("backgroundImage",d.apply(this,b))})},a.fn.rainbowGradient=function(b,c){var d,e,f,g;for(b=b||"top",d=a.extend({},{s:100,l:50},c),e="hsl(%h%,"+d.s+"%,"+d.l+"%)",f=0,g=[];f<=360;)g.push(e.replace("%h%",f)),f+=30;return this.each(function(){a(this).gradient(b,g)})},n={options:{color:!1,mode:"hsl",controls:{horiz:"s",vert:"l",strip:"h"},hide:!0,border:!0,target:!1,width:200,palettes:!1,type:"full",slider:"horizontal"},_color:"",_palettes:["#000","#fff","#d33","#d93","#ee2","#81d742","#1e73be","#8224e3"],_inited:!1,_defaultHSLControls:{horiz:"s",vert:"l",strip:"h"},_defaultHSVControls:{horiz:"h",vert:"v",strip:"s"},_scale:{h:360,s:100,l:100,v:100},_create:function(){var b=this,d=b.element,e=b.options.color||d.val();!1===k&&c(),d.is("input")?(b.options.target?b.picker=a(i).appendTo(b.options.target):b.picker=a(i).insertAfter(d),b._addInputListeners(d)):(d.append(i),b.picker=d.find(".iris-picker")),p?9===q?b.picker.addClass("iris-ie-9"):q<=8&&b.picker.addClass("iris-ie-lt9"):o.indexOf("compatible")<0&&o.indexOf("khtml")<0&&o.match(/mozilla/)&&b.picker.addClass("iris-mozilla"),b.options.palettes&&b._addPalettes(),b.onlySlider="hue"===b.options.type,b.horizontalSlider=b.onlySlider&&"horizontal"===b.options.slider,b.onlySlider&&(b.options.controls.strip="h",e||(e="hsl(10,100,50)")),b._color=new Color(e).setHSpace(b.options.mode),b.options.color=b._color.toString(),b.controls={square:b.picker.find(".iris-square"),squareDrag:b.picker.find(".iris-square-value"),horiz:b.picker.find(".iris-square-horiz"),vert:b.picker.find(".iris-square-vert"),strip:b.picker.find(".iris-strip"),stripSlider:b.picker.find(".iris-strip .iris-slider-offset")},"hsv"===b.options.mode&&b._has("l",b.options.controls)?b.options.controls=b._defaultHSVControls:"hsl"===b.options.mode&&b._has("v",b.options.controls)&&(b.options.controls=b._defaultHSLControls),b.hue=b._color.h(),b.options.hide&&b.picker.hide(),b.options.border&&!b.onlySlider&&b.picker.addClass("iris-border"),b._initControls(),b.active="external",b._dimensions(),b._change()},_has:function(b,c){var d=!1;return a.each(c,function(a,c){if(b===c)return d=!0,!1}),d},_addPalettes:function(){var b=a('<div class="iris-palette-container" />'),c=a('<a class="iris-palette" tabindex="0" />'),d=Array.isArray(this.options.palettes)?this.options.palettes:this._palettes;this.picker.find(".iris-palette-container").length&&(b=this.picker.find(".iris-palette-container").detach().html("")),a.each(d,function(a,d){c.clone().data("color",d).css("backgroundColor",d).appendTo(b).height(10).width(10)}),this.picker.append(b)},_paint:function(){var a=this;a.horizontalSlider?a._paintDimension("left","strip"):a._paintDimension("top","strip"),a._paintDimension("top","vert"),a._paintDimension("left","horiz")},_paintDimension:function(a,b){var c,d=this,e=d._color,f=d.options.mode,g=d._getHSpaceColor(),h=d.controls[b],i=d.options.controls;if(b!==d.active&&("square"!==d.active||"strip"===b))switch(i[b]){case"h":if("hsv"===f){switch(g=e.clone(),b){case"horiz":g[i.vert](100);break;case"vert":g[i.horiz](100);break;case"strip":g.setHSpace("hsl")}c=g.toHsl()}else c="strip"===b?{s:g.s,l:g.l}:{s:100,l:g.l};h.rainbowGradient(a,c);break;case"s":"hsv"===f?"vert"===b?c=[e.clone().a(0).s(0).toCSS("rgba"),e.clone().a(1).s(0).toCSS("rgba")]:"strip"===b?c=[e.clone().s(100).toCSS("hsl"),e.clone().s(0).toCSS("hsl")]:"horiz"===b&&(c=["#fff","hsl("+g.h+",100%,50%)"]):c="vert"===b&&"h"===d.options.controls.horiz?["hsla(0, 0%, "+g.l+"%, 0)","hsla(0, 0%, "+g.l+"%, 1)"]:["hsl("+g.h+",0%,50%)","hsl("+g.h+",100%,50%)"],h.gradient(a,c);break;case"l":c="strip"===b?["hsl("+g.h+",100%,100%)","hsl("+g.h+", "+g.s+"%,50%)","hsl("+g.h+",100%,0%)"]:["#fff","rgba(255,255,255,0) 50%","rgba(0,0,0,0) 50%","rgba(0,0,0,1)"],h.gradient(a,c);break;case"v":c="strip"===b?[e.clone().v(100).toCSS(),e.clone().v(0).toCSS()]:["rgba(0,0,0,0)","#000"],h.gradient(a,c)}},_getHSpaceColor:function(){return"hsv"===this.options.mode?this._color.toHsv():this._color.toHsl()},_stripOnlyDimensions:function(){var a=this,b=this.options.width,c=.12*b;a.horizontalSlider?a.picker.css({width:b,height:c}).addClass("iris-only-strip iris-strip-horiz"):a.picker.css({width:c,height:b}).addClass("iris-only-strip iris-strip-vert")},_dimensions:function(b){if("hue"===this.options.type)return this._stripOnlyDimensions();var c,d,e,f,g=this,h=g.options,i=g.controls,j=i.square,k=g.picker.find(".iris-strip"),l="77.5%",m="12%",n=20,o=h.border?h.width-n:h.width,p=Array.isArray(h.palettes)?h.palettes.length:g._palettes.length;if(b&&(j.css("width",""),k.css("width",""),g.picker.css({width:"",height:""})),l=o*(parseFloat(l)/100),m=o*(parseFloat(m)/100),c=h.border?l+n:l,j.width(l).height(l),k.height(l).width(m),g.picker.css({width:h.width,height:c}),!h.palettes)return g.picker.css("paddingBottom","");d=2*l/100,f=l-(p-1)*d,e=f/p,g.picker.find(".iris-palette").each(function(b){var c=0===b?0:d;a(this).css({width:e,height:e,marginLeft:c})}),g.picker.css("paddingBottom",e+d),k.height(e+d+l)},_addInputListeners:function(a){var b=this,c=function(c){var d=new Color(a.val()),e=a.val().replace(/^#/,"");a.removeClass("iris-error"),d.error?""!==e&&a.addClass("iris-error"):d.toString()!==b._color.toString()&&("keyup"===c.type&&e.match(/^[0-9a-fA-F]{3}$/)||b._setOption("color",d.toString()))};a.on("change",c).on("keyup",b._debounce(c,100)),b.options.hide&&a.one("focus",function(){b.show()})},_initControls:function(){var b=this,c=b.controls,d=c.square,e=b.options.controls,f=b._scale[e.strip],g=b.horizontalSlider?"horizontal":"vertical";c.stripSlider.slider({orientation:g,max:f,slide:function(a,c){b.active="strip","h"===e.strip&&"vertical"===g&&(c.value=f-c.value),b._color[e.strip](c.value),b._change.apply(b,arguments)}}),c.squareDrag.draggable({containment:c.square.find(".iris-square-inner"),zIndex:1e3,cursor:"move",drag:function(a,c){b._squareDrag(a,c)},start:function(){d.addClass("iris-dragging"),a(this).addClass("ui-state-focus")},stop:function(){d.removeClass("iris-dragging"),a(this).removeClass("ui-state-focus")}}).on("mousedown mouseup",function(c){var d="ui-state-focus";c.preventDefault(),"mousedown"===c.type?(b.picker.find("."+d).removeClass(d).trigger("blur"),a(this).addClass(d).trigger("focus")):a(this).removeClass(d)}).on("keydown",function(a){var d=c.square,e=c.squareDrag,f=e.position(),g=b.options.width/100;switch(a.altKey&&(g*=10),a.keyCode){case 37:f.left-=g;break;case 38:f.top-=g;break;case 39:f.left+=g;break;case 40:f.top+=g;break;default:return!0}f.left=Math.max(0,Math.min(f.left,d.width())),f.top=Math.max(0,Math.min(f.top,d.height())),e.css(f),b._squareDrag(a,{position:f}),a.preventDefault()}),d.on("mousedown",function(c){var d,e;1===c.which&&a(c.target).is("div")&&(d=b.controls.square.offset(),e={top:c.pageY-d.top,left:c.pageX-d.left},c.preventDefault(),b._squareDrag(c,{position:e}),c.target=b.controls.squareDrag.get(0),b.controls.squareDrag.css(e).trigger(c))}),b.options.palettes&&b._paletteListeners()},_paletteListeners:function(){var b=this;b.picker.find(".iris-palette-container").on("click.palette",".iris-palette",function(){b._color.fromCSS(a(this).data("color")),b.active="external",b._change()}).on("keydown.palette",".iris-palette",function(b){if(13!==b.keyCode&&32!==b.keyCode)return!0;b.stopPropagation(),a(this).trigger("click")})},_squareDrag:function(a,b){var c=this,d=c.options.controls,e=c._squareDimensions(),f=Math.round((e.h-b.position.top)/e.h*c._scale[d.vert]),g=c._scale[d.horiz]-Math.round((e.w-b.position.left)/e.w*c._scale[d.horiz]);c._color[d.horiz](g)[d.vert](f),c.active="square",c._change.apply(c,arguments)},_setOption:function(b,c){var d,e,f=this,g=f.options[b],h=!1;switch(f.options[b]=c,b){case"color":f.onlySlider?(c=parseInt(c,10),c=isNaN(c)||c<0||c>359?g:"hsl("+c+",100,50)",f.options.color=f.options[b]=c,f._color=new Color(c).setHSpace(f.options.mode),f.active="external",f._change()):(c=""+c,c.replace(/^#/,""),d=new Color(c).setHSpace(f.options.mode),d.error?f.options[b]=g:(f._color=d,f.options.color=f.options[b]=f._color.toString(),f.active="external",f._change()));break;case"palettes":h=!0,c?f._addPalettes():f.picker.find(".iris-palette-container").remove(),g||f._paletteListeners();break;case"width":h=!0;break;case"border":h=!0,e=c?"addClass":"removeClass",f.picker[e]("iris-border");break;case"mode":case"controls":if(g===c)return;return e=f.element,g=f.options,g.hide=!f.picker.is(":visible"),f.destroy(),f.picker.remove(),a(f.element).iris(g)}h&&f._dimensions(!0)},_squareDimensions:function(a){var c,d=this.controls.square;return a!==b&&d.data("dimensions")?d.data("dimensions"):(this.controls.squareDrag,c={w:d.width(),h:d.height()},d.data("dimensions",c),c)},_isNonHueControl:function(a,b){return"square"===a&&"h"===this.options.controls.strip||"external"!==b&&("h"!==b||"strip"!==a)},_change:function(){var b=this,c=b.controls,d=b._getHSpaceColor(),e=["square","strip"],f=b.options.controls,g=f[b.active]||"external",h=b.hue;"strip"===b.active?e=[]:"external"!==b.active&&e.pop(),a.each(e,function(a,e){var g,h,i;if(e!==b.active)switch(e){case"strip":g="h"!==f.strip||b.horizontalSlider?d[f.strip]:b._scale[f.strip]-d[f.strip],c.stripSlider.slider("value",g);break;case"square":h=b._squareDimensions(),i={left:d[f.horiz]/b._scale[f.horiz]*h.w,top:h.h-d[f.vert]/b._scale[f.vert]*h.h},b.controls.squareDrag.css(i)}}),d.h!==h&&b._isNonHueControl(b.active,g)&&b._color.h(h),b.hue=b._color.h(),b.options.color=b._color.toString(),b._inited&&b._trigger("change",{type:b.active},{color:b._color}),b.element.is(":input")&&!b._color.error&&(b.element.removeClass("iris-error"),b.onlySlider?b.element.val()!==b.hue&&b.element.val(b.hue):b.element.val()!==b._color.toString()&&b.element.val(b._color.toString())),b._paint(),b._inited=!0,b.active=!1},_debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},show:function(){this.picker.show()},hide:function(){this.picker.hide()},toggle:function(){this.picker.toggle()},color:function(a){return!0===a?this._color.clone():a===b?this._color.toString():void this.option("color",a)}},a.widget("a8c.iris",n),a('<style id="iris-css">'+m+"</style>").appendTo("head")}(jQuery),function(a,b){var c=function(a,b){return this instanceof c?this._init(a,b):new c(a,b)};c.fn=c.prototype={_color:0,_alpha:1,error:!1,_hsl:{h:0,s:0,l:0},_hsv:{h:0,s:0,v:0},_hSpace:"hsl",_init:function(a){var c="noop";switch(typeof a){case"object":return a.a!==b&&this.a(a.a),c=a.r!==b?"fromRgb":a.l!==b?"fromHsl":a.v!==b?"fromHsv":c,this[c](a);case"string":return this.fromCSS(a);case"number":return this.fromInt(parseInt(a,10))}return this},_error:function(){return this.error=!0,this},clone:function(){for(var a=new c(this.toInt()),b=["_alpha","_hSpace","_hsl","_hsv","error"],d=b.length-1;d>=0;d--)a[b[d]]=this[b[d]];return a},setHSpace:function(a){return this._hSpace="hsv"===a?a:"hsl",this},noop:function(){return this},fromCSS:function(a){var b,c=/^(rgb|hs(l|v))a?\(/;if(this.error=!1,a=a.replace(/^\s+/,"").replace(/\s+$/,"").replace(/;$/,""),a.match(c)&&a.match(/\)$/)){if(b=a.replace(/(\s|%)/g,"").replace(c,"").replace(/,?\);?$/,"").split(","),b.length<3)return this._error();if(4===b.length&&(this.a(parseFloat(b.pop())),this.error))return this;for(var d=b.length-1;d>=0;d--)if(b[d]=parseInt(b[d],10),isNaN(b[d]))return this._error();return a.match(/^rgb/)?this.fromRgb({r:b[0],g:b[1],b:b[2]}):a.match(/^hsv/)?this.fromHsv({h:b[0],s:b[1],v:b[2]}):this.fromHsl({h:b[0],s:b[1],l:b[2]})}return this.fromHex(a)},fromRgb:function(a,c){return"object"!=typeof a||a.r===b||a.g===b||a.b===b?this._error():(this.error=!1,this.fromInt(parseInt((a.r<<16)+(a.g<<8)+a.b,10),c))},fromHex:function(a){return a=a.replace(/^#/,"").replace(/^0x/,""),3===a.length&&(a=a[0]+a[0]+a[1]+a[1]+a[2]+a[2]),this.error=!/^[0-9A-F]{6}$/i.test(a),this.fromInt(parseInt(a,16))},fromHsl:function(a){var c,d,e,f,g,h,i,j;return"object"!=typeof a||a.h===b||a.s===b||a.l===b?this._error():(this._hsl=a,this._hSpace="hsl",h=a.h/360,i=a.s/100,j=a.l/100,0===i?c=d=e=j:(f=j<.5?j*(1+i):j+i-j*i,g=2*j-f,c=this.hue2rgb(g,f,h+1/3),d=this.hue2rgb(g,f,h),e=this.hue2rgb(g,f,h-1/3)),this.fromRgb({r:255*c,g:255*d,b:255*e},!0))},fromHsv:function(a){var c,d,e,f,g,h,i,j,k,l,m;if("object"!=typeof a||a.h===b||a.s===b||a.v===b)return this._error();switch(this._hsv=a,this._hSpace="hsv",c=a.h/360,d=a.s/100,e=a.v/100,i=Math.floor(6*c),j=6*c-i,k=e*(1-d),l=e*(1-j*d),m=e*(1-(1-j)*d),i%6){case 0:f=e,g=m,h=k;break;case 1:f=l,g=e,h=k;break;case 2:f=k,g=e,h=m;break;case 3:f=k,g=l,h=e;break;case 4:f=m,g=k,h=e;break;case 5:f=e,g=k,h=l}return this.fromRgb({r:255*f,g:255*g,b:255*h},!0)},fromInt:function(a,c){return this._color=parseInt(a,10),isNaN(this._color)&&(this._color=0),this._color>16777215?this._color=16777215:this._color<0&&(this._color=0),c===b&&(this._hsv.h=this._hsv.s=this._hsl.h=this._hsl.s=0),this},hue2rgb:function(a,b,c){return c<0&&(c+=1),c>1&&(c-=1),c<1/6?a+6*(b-a)*c:c<.5?b:c<2/3?a+(b-a)*(2/3-c)*6:a},toString:function(){var a=parseInt(this._color,10).toString(16);if(this.error)return"";if(a.length<6)for(var b=6-a.length-1;b>=0;b--)a="0"+a;return"#"+a},toCSS:function(a,b){switch(a=a||"hex",b=parseFloat(b||this._alpha),a){case"rgb":case"rgba":var c=this.toRgb();return b<1?"rgba( "+c.r+", "+c.g+", "+c.b+", "+b+" )":"rgb( "+c.r+", "+c.g+", "+c.b+" )";case"hsl":case"hsla":var d=this.toHsl();return b<1?"hsla( "+d.h+", "+d.s+"%, "+d.l+"%, "+b+" )":"hsl( "+d.h+", "+d.s+"%, "+d.l+"% )";default:return this.toString()}},toRgb:function(){return{r:255&this._color>>16,g:255&this._color>>8,b:255&this._color}},toHsl:function(){var a,b,c=this.toRgb(),d=c.r/255,e=c.g/255,f=c.b/255,g=Math.max(d,e,f),h=Math.min(d,e,f),i=(g+h)/2;if(g===h)a=b=0;else{var j=g-h;switch(b=i>.5?j/(2-g-h):j/(g+h),g){case d:a=(e-f)/j+(e<f?6:0);break;case e:a=(f-d)/j+2;break;case f:a=(d-e)/j+4}a/=6}return a=Math.round(360*a),0===a&&this._hsl.h!==a&&(a=this._hsl.h),b=Math.round(100*b),0===b&&this._hsl.s&&(b=this._hsl.s),{h:a,s:b,l:Math.round(100*i)}},toHsv:function(){var a,b,c=this.toRgb(),d=c.r/255,e=c.g/255,f=c.b/255,g=Math.max(d,e,f),h=Math.min(d,e,f),i=g,j=g-h;if(b=0===g?0:j/g,g===h)a=b=0;else{switch(g){case d:a=(e-f)/j+(e<f?6:0);break;case e:a=(f-d)/j+2;break;case f:a=(d-e)/j+4}a/=6}return a=Math.round(360*a),0===a&&this._hsv.h!==a&&(a=this._hsv.h),b=Math.round(100*b),0===b&&this._hsv.s&&(b=this._hsv.s),{h:a,s:b,v:Math.round(100*i)}},toInt:function(){return this._color},toIEOctoHex:function(){var a=this.toString(),b=parseInt(255*this._alpha,10).toString(16);return 1===b.length&&(b="0"+b),"#"+b+a.replace(/^#/,"")},toLuminosity:function(){var a=this.toRgb(),b={};for(var c in a)if(a.hasOwnProperty(c)){var d=a[c]/255;b[c]=d<=.03928?d/12.92:Math.pow((d+.055)/1.055,2.4)}return.2126*b.r+.7152*b.g+.0722*b.b},getDistanceLuminosityFrom:function(a){if(!(a instanceof c))throw"getDistanceLuminosityFrom requires a Color object";var b=this.toLuminosity(),d=a.toLuminosity();return b>d?(b+.05)/(d+.05):(d+.05)/(b+.05)},getMaxContrastColor:function(){var a=this.getDistanceLuminosityFrom(new c("#000")),b=this.getDistanceLuminosityFrom(new c("#fff"));return new c(a>=b?"#000":"#fff")},getReadableContrastingColor:function(a,d){if(!(a instanceof c))return this;var e,f,g=d===b?5:d,h=a.getDistanceLuminosityFrom(this);if(h>=g)return this;if(e=a.getMaxContrastColor(),e.getDistanceLuminosityFrom(a)<=g)return e;for(f=0===e.toInt()?-1:1;h<g&&(this.l(f,!0),h=this.getDistanceLuminosityFrom(a),0!==this._color&&16777215!==this._color););return this},a:function(a){if(a===b)return this._alpha;var c=parseFloat(a);return isNaN(c)?this._error():(this._alpha=c,this)},darken:function(a){return a=a||5,this.l(-a,!0)},lighten:function(a){return a=a||5,this.l(a,!0)},saturate:function(a){return a=a||15,this.s(a,!0)},desaturate:function(a){return a=a||15,this.s(-a,!0)},toGrayscale:function(){return this.setHSpace("hsl").s(0)},getComplement:function(){return this.h(180,!0)},getSplitComplement:function(a){a=a||1;var b=180+30*a;return this.h(b,!0)},getAnalog:function(a){a=a||1;var b=30*a;return this.h(b,!0)},getTetrad:function(a){a=a||1;var b=60*a;return this.h(b,!0)},getTriad:function(a){a=a||1;var b=120*a;return this.h(b,!0)},_partial:function(a){var c=d[a];return function(d,e){var f=this._spaceFunc("to",c.space);return d===b?f[a]:(!0===e&&(d=f[a]+d),c.mod&&(d%=c.mod),c.range&&(d=d<c.range[0]?c.range[0]:d>c.range[1]?c.range[1]:d),f[a]=d,this._spaceFunc("from",c.space,f))}},_spaceFunc:function(a,b,c){var d=b||this._hSpace;return this[a+d.charAt(0).toUpperCase()+d.substr(1)](c)}};var d={h:{mod:360},s:{range:[0,100]},l:{space:"hsl",range:[0,100]},v:{space:"hsv",range:[0,100]},r:{space:"rgb",range:[0,255]},g:{space:"rgb",range:[0,255]},b:{space:"rgb",range:[0,255]}};for(var e in d)d.hasOwnProperty(e)&&(c.fn[e]=c.fn._partial(e));"object"==typeof exports?module.exports=c:a.Color=c}(this);
\ No newline at end of file diff --git a/wp-admin/js/language-chooser.js b/wp-admin/js/language-chooser.js new file mode 100644 index 0000000..7740d16 --- /dev/null +++ b/wp-admin/js/language-chooser.js @@ -0,0 +1,36 @@ +/** + * @output wp-admin/js/language-chooser.js + */ + +jQuery( function($) { +/* + * Set the correct translation to the continue button and show a spinner + * when downloading a language. + */ +var select = $( '#language' ), + submit = $( '#language-continue' ); + +if ( ! $( 'body' ).hasClass( 'language-chooser' ) ) { + return; +} + +select.trigger( 'focus' ).on( 'change', function() { + /* + * When a language is selected, set matching translation to continue button + * and attach the language attribute. + */ + var option = select.children( 'option:selected' ); + submit.attr({ + value: option.data( 'continue' ), + lang: option.attr( 'lang' ) + }); +}); + +$( 'form' ).on( 'submit', function() { + // Show spinner for languages that need to be downloaded. + if ( ! select.children( 'option:selected' ).data( 'installed' ) ) { + $( this ).find( '.step .spinner' ).css( 'visibility', 'visible' ); + } +}); + +}); diff --git a/wp-admin/js/language-chooser.min.js b/wp-admin/js/language-chooser.min.js new file mode 100644 index 0000000..835b911 --- /dev/null +++ b/wp-admin/js/language-chooser.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(n){var e=n("#language"),a=n("#language-continue");n("body").hasClass("language-chooser")&&(e.trigger("focus").on("change",function(){var n=e.children("option:selected");a.attr({value:n.data("continue"),lang:n.attr("lang")})}),n("form").on("submit",function(){e.children("option:selected").data("installed")||n(this).find(".step .spinner").css("visibility","visible")}))});
\ No newline at end of file diff --git a/wp-admin/js/link.js b/wp-admin/js/link.js new file mode 100644 index 0000000..1456ba9 --- /dev/null +++ b/wp-admin/js/link.js @@ -0,0 +1,140 @@ +/** + * @output wp-admin/js/link.js + */ + +/* global postboxes, deleteUserSetting, setUserSetting, getUserSetting */ + +jQuery( function($) { + + var newCat, noSyncChecks = false, syncChecks, catAddAfter; + + $('#link_name').trigger( 'focus' ); + // Postboxes. + postboxes.add_postbox_toggles('link'); + + /** + * Adds event that opens a particular category tab. + * + * @ignore + * + * @return {boolean} Always returns false to prevent the default behavior. + */ + $('#category-tabs a').on( 'click', function(){ + var t = $(this).attr('href'); + $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); + $('.tabs-panel').hide(); + $(t).show(); + if ( '#categories-all' == t ) + deleteUserSetting('cats'); + else + setUserSetting('cats','pop'); + return false; + }); + if ( getUserSetting('cats') ) + $('#category-tabs a[href="#categories-pop"]').trigger( 'click' ); + + // Ajax Cat. + newCat = $('#newcat').one( 'focus', function() { $(this).val( '' ).removeClass( 'form-input-tip' ); } ); + + /** + * After adding a new category, focus on the category add input field. + * + * @return {void} + */ + $('#link-category-add-submit').on( 'click', function() { newCat.focus(); } ); + + /** + * Synchronize category checkboxes. + * + * This function makes sure that the checkboxes are synced between the all + * categories tab and the most used categories tab. + * + * @since 2.5.0 + * + * @return {void} + */ + syncChecks = function() { + if ( noSyncChecks ) + return; + noSyncChecks = true; + var th = $(this), c = th.is(':checked'), id = th.val().toString(); + $('#in-link-category-' + id + ', #in-popular-link_category-' + id).prop( 'checked', c ); + noSyncChecks = false; + }; + + /** + * Adds event listeners to an added category. + * + * This is run on the addAfter event to make sure the correct event listeners + * are bound to the DOM elements. + * + * @since 2.5.0 + * + * @param {string} r Raw XML response returned from the server after adding a + * category. + * @param {Object} s List manager configuration object; settings for the Ajax + * request. + * + * @return {void} + */ + catAddAfter = function( r, s ) { + $(s.what + ' response_data', r).each( function() { + var t = $($(this).text()); + t.find( 'label' ).each( function() { + var th = $(this), + val = th.find('input').val(), + id = th.find('input')[0].id, + name = th.text().trim(), + o; + $('#' + id).on( 'change', syncChecks ); + o = $( '<option value="' + parseInt( val, 10 ) + '"></option>' ).text( name ); + } ); + } ); + }; + + /* + * Instantiates the list manager. + * + * @see js/_enqueues/lib/lists.js + */ + $('#categorychecklist').wpList( { + // CSS class name for alternate styling. + alt: '', + + // The type of list. + what: 'link-category', + + // ID of the element the parsed Ajax response will be stored in. + response: 'category-ajax-response', + + // Callback that's run after an item got added to the list. + addAfter: catAddAfter + } ); + + // All categories is the default tab, so we delete the user setting. + $('a[href="#categories-all"]').on( 'click', function(){deleteUserSetting('cats');}); + + // Set a preference for the popular categories to cookies. + $('a[href="#categories-pop"]').on( 'click', function(){setUserSetting('cats','pop');}); + + if ( 'pop' == getUserSetting('cats') ) + $('a[href="#categories-pop"]').trigger( 'click' ); + + /** + * Adds event handler that shows the interface controls to add a new category. + * + * @ignore + * + * @param {Event} event The event object. + * @return {boolean} Always returns false to prevent regular link + * functionality. + */ + $('#category-add-toggle').on( 'click', function() { + $(this).parents('div:first').toggleClass( 'wp-hidden-children' ); + $('#category-tabs a[href="#categories-all"]').trigger( 'click' ); + $('#newcategory').trigger( 'focus' ); + return false; + } ); + + $('.categorychecklist :checkbox').on( 'change', syncChecks ).filter( ':checked' ).trigger( 'change' ); +}); diff --git a/wp-admin/js/link.min.js b/wp-admin/js/link.min.js new file mode 100644 index 0000000..e8136ab --- /dev/null +++ b/wp-admin/js/link.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(a){var t,c,e,i=!1;a("#link_name").trigger("focus"),postboxes.add_postbox_toggles("link"),a("#category-tabs a").on("click",function(){var t=a(this).attr("href");return a(this).parent().addClass("tabs").siblings("li").removeClass("tabs"),a(".tabs-panel").hide(),a(t).show(),"#categories-all"==t?deleteUserSetting("cats"):setUserSetting("cats","pop"),!1}),getUserSetting("cats")&&a('#category-tabs a[href="#categories-pop"]').trigger("click"),t=a("#newcat").one("focus",function(){a(this).val("").removeClass("form-input-tip")}),a("#link-category-add-submit").on("click",function(){t.focus()}),c=function(){var t,e;i||(i=!0,t=(e=a(this)).is(":checked"),e=e.val().toString(),a("#in-link-category-"+e+", #in-popular-link_category-"+e).prop("checked",t),i=!1)},e=function(t,e){a(e.what+" response_data",t).each(function(){a(a(this).text()).find("label").each(function(){var t=a(this),e=t.find("input").val(),i=t.find("input")[0].id,t=t.text().trim();a("#"+i).on("change",c),a('<option value="'+parseInt(e,10)+'"></option>').text(t)})})},a("#categorychecklist").wpList({alt:"",what:"link-category",response:"category-ajax-response",addAfter:e}),a('a[href="#categories-all"]').on("click",function(){deleteUserSetting("cats")}),a('a[href="#categories-pop"]').on("click",function(){setUserSetting("cats","pop")}),"pop"==getUserSetting("cats")&&a('a[href="#categories-pop"]').trigger("click"),a("#category-add-toggle").on("click",function(){return a(this).parents("div:first").toggleClass("wp-hidden-children"),a('#category-tabs a[href="#categories-all"]').trigger("click"),a("#newcategory").trigger("focus"),!1}),a(".categorychecklist :checkbox").on("change",c).filter(":checked").trigger("change")});
\ No newline at end of file diff --git a/wp-admin/js/media-gallery.js b/wp-admin/js/media-gallery.js new file mode 100644 index 0000000..6df4c37 --- /dev/null +++ b/wp-admin/js/media-gallery.js @@ -0,0 +1,43 @@ +/** + * This file is used on media-upload.php which has been replaced by media-new.php and upload.php + * + * @deprecated 3.5.0 + * @output wp-admin/js/media-gallery.js + */ + + /* global ajaxurl */ +jQuery(function($) { + /** + * Adds a click event handler to the element with a 'wp-gallery' class. + */ + $( 'body' ).on( 'click.wp-gallery', function(e) { + var target = $( e.target ), id, img_size, nonceValue; + + if ( target.hasClass( 'wp-set-header' ) ) { + // Opens the image to preview it full size. + ( window.dialogArguments || opener || parent || top ).location.href = target.data( 'location' ); + e.preventDefault(); + } else if ( target.hasClass( 'wp-set-background' ) ) { + // Sets the image as background of the theme. + id = target.data( 'attachment-id' ); + img_size = $( 'input[name="attachments[' + id + '][image-size]"]:checked').val(); + nonceValue = $( '#_wpnonce' ).val() && ''; + + /** + * This Ajax action has been deprecated since 3.5.0, see custom-background.php + */ + jQuery.post(ajaxurl, { + action: 'set-background-image', + attachment_id: id, + _ajax_nonce: nonceValue, + size: img_size + }, function() { + var win = window.dialogArguments || opener || parent || top; + win.tb_remove(); + win.location.reload(); + }); + + e.preventDefault(); + } + }); +}); diff --git a/wp-admin/js/media-gallery.min.js b/wp-admin/js/media-gallery.min.js new file mode 100644 index 0000000..088dbe6 --- /dev/null +++ b/wp-admin/js/media-gallery.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(o){o("body").on("click.wp-gallery",function(a){var e,t,n=o(a.target);n.hasClass("wp-set-header")?((window.dialogArguments||opener||parent||top).location.href=n.data("location"),a.preventDefault()):n.hasClass("wp-set-background")&&(n=n.data("attachment-id"),e=o('input[name="attachments['+n+'][image-size]"]:checked').val(),t=o("#_wpnonce").val()&&"",jQuery.post(ajaxurl,{action:"set-background-image",attachment_id:n,_ajax_nonce:t,size:e},function(){var a=window.dialogArguments||opener||parent||top;a.tb_remove(),a.location.reload()}),a.preventDefault())})});
\ No newline at end of file diff --git a/wp-admin/js/media-upload.js b/wp-admin/js/media-upload.js new file mode 100644 index 0000000..fb62046 --- /dev/null +++ b/wp-admin/js/media-upload.js @@ -0,0 +1,113 @@ +/** + * Contains global functions for the media upload within the post edit screen. + * + * Updates the ThickBox anchor href and the ThickBox's own properties in order + * to set the size and position on every resize event. Also adds a function to + * send HTML or text to the currently active editor. + * + * @file + * @since 2.5.0 + * @output wp-admin/js/media-upload.js + * + * @requires jQuery + */ + +/* global tinymce, QTags, wpActiveEditor, tb_position */ + +/** + * Sends the HTML passed in the parameters to TinyMCE. + * + * @since 2.5.0 + * + * @global + * + * @param {string} html The HTML to be sent to the editor. + * @return {void|boolean} Returns false when both TinyMCE and QTags instances + * are unavailable. This means that the HTML was not + * sent to the editor. + */ +window.send_to_editor = function( html ) { + var editor, + hasTinymce = typeof tinymce !== 'undefined', + hasQuicktags = typeof QTags !== 'undefined'; + + // If no active editor is set, try to set it. + if ( ! wpActiveEditor ) { + if ( hasTinymce && tinymce.activeEditor ) { + editor = tinymce.activeEditor; + window.wpActiveEditor = editor.id; + } else if ( ! hasQuicktags ) { + return false; + } + } else if ( hasTinymce ) { + editor = tinymce.get( wpActiveEditor ); + } + + // If the editor is set and not hidden, + // insert the HTML into the content of the editor. + if ( editor && ! editor.isHidden() ) { + editor.execCommand( 'mceInsertContent', false, html ); + } else if ( hasQuicktags ) { + // If quick tags are available, insert the HTML into its content. + QTags.insertContent( html ); + } else { + // If neither the TinyMCE editor and the quick tags are available, + // add the HTML to the current active editor. + document.getElementById( wpActiveEditor ).value += html; + } + + // If the old thickbox remove function exists, call it. + if ( window.tb_remove ) { + try { window.tb_remove(); } catch( e ) {} + } +}; + +(function($) { + /** + * Recalculates and applies the new ThickBox position based on the current + * window size. + * + * @since 2.6.0 + * + * @global + * + * @return {Object[]} Array containing jQuery objects for all the found + * ThickBox anchors. + */ + window.tb_position = function() { + var tbWindow = $('#TB_window'), + width = $(window).width(), + H = $(window).height(), + W = ( 833 < width ) ? 833 : width, + adminbar_height = 0; + + if ( $('#wpadminbar').length ) { + adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 ); + } + + if ( tbWindow.length ) { + 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'}); + } + + /** + * Recalculates the new height and width for all links with a ThickBox class. + * + * @since 2.6.0 + */ + return $('a.thickbox').each( function() { + var href = $(this).attr('href'); + if ( ! href ) return; + href = href.replace(/&width=[0-9]+/g, ''); + href = href.replace(/&height=[0-9]+/g, ''); + $(this).attr( 'href', href + '&width=' + ( W - 80 ) + '&height=' + ( H - 85 - adminbar_height ) ); + }); + }; + + // Add handler to recalculates the ThickBox position when the window is resized. + $(window).on( 'resize', function(){ tb_position(); }); + +})(jQuery); diff --git a/wp-admin/js/media-upload.min.js b/wp-admin/js/media-upload.min.js new file mode 100644 index 0000000..ca867fc --- /dev/null +++ b/wp-admin/js/media-upload.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.send_to_editor=function(t){var e,i="undefined"!=typeof tinymce,n="undefined"!=typeof QTags;if(wpActiveEditor)i&&(e=tinymce.get(wpActiveEditor));else if(i&&tinymce.activeEditor)e=tinymce.activeEditor,window.wpActiveEditor=e.id;else if(!n)return!1;if(e&&!e.isHidden()?e.execCommand("mceInsertContent",!1,t):n?QTags.insertContent(t):document.getElementById(wpActiveEditor).value+=t,window.tb_remove)try{window.tb_remove()}catch(t){}},function(d){window.tb_position=function(){var t=d("#TB_window"),e=d(window).width(),i=d(window).height(),n=833<e?833:e,o=0;return d("#wpadminbar").length&&(o=parseInt(d("#wpadminbar").css("height"),10)),t.length&&(t.width(n-50).height(i-45-o),d("#TB_iframeContent").width(n-50).height(i-75-o),t.css({"margin-left":"-"+parseInt((n-50)/2,10)+"px"}),void 0!==document.body.style.maxWidth)&&t.css({top:20+o+"px","margin-top":"0"}),d("a.thickbox").each(function(){var t=d(this).attr("href");t&&(t=(t=t.replace(/&width=[0-9]+/g,"")).replace(/&height=[0-9]+/g,""),d(this).attr("href",t+"&width="+(n-80)+"&height="+(i-85-o)))})},d(window).on("resize",function(){tb_position()})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/media.js b/wp-admin/js/media.js new file mode 100644 index 0000000..648ade5 --- /dev/null +++ b/wp-admin/js/media.js @@ -0,0 +1,242 @@ +/** + * Creates a dialog containing posts that can have a particular media attached + * to it. + * + * @since 2.7.0 + * @output wp-admin/js/media.js + * + * @namespace findPosts + * + * @requires jQuery + */ + +/* global ajaxurl, _wpMediaGridSettings, showNotice, findPosts, ClipboardJS */ + +( function( $ ){ + window.findPosts = { + /** + * Opens a dialog to attach media to a post. + * + * Adds an overlay prior to retrieving a list of posts to attach the media to. + * + * @since 2.7.0 + * + * @memberOf findPosts + * + * @param {string} af_name The name of the affected element. + * @param {string} af_val The value of the affected post element. + * + * @return {boolean} Always returns false. + */ + open: function( af_name, af_val ) { + var overlay = $( '.ui-find-overlay' ); + + if ( overlay.length === 0 ) { + $( 'body' ).append( '<div class="ui-find-overlay"></div>' ); + findPosts.overlay(); + } + + overlay.show(); + + if ( af_name && af_val ) { + // #affected is a hidden input field in the dialog that keeps track of which media should be attached. + $( '#affected' ).attr( 'name', af_name ).val( af_val ); + } + + $( '#find-posts' ).show(); + + // Close the dialog when the escape key is pressed. + $('#find-posts-input').trigger( 'focus' ).on( 'keyup', function( event ){ + if ( event.which == 27 ) { + findPosts.close(); + } + }); + + // Retrieves a list of applicable posts for media attachment and shows them. + findPosts.send(); + + return false; + }, + + /** + * Clears the found posts lists before hiding the attach media dialog. + * + * @since 2.7.0 + * + * @memberOf findPosts + * + * @return {void} + */ + close: function() { + $('#find-posts-response').empty(); + $('#find-posts').hide(); + $( '.ui-find-overlay' ).hide(); + }, + + /** + * Binds a click event listener to the overlay which closes the attach media + * dialog. + * + * @since 3.5.0 + * + * @memberOf findPosts + * + * @return {void} + */ + overlay: function() { + $( '.ui-find-overlay' ).on( 'click', function () { + findPosts.close(); + }); + }, + + /** + * Retrieves and displays posts based on the search term. + * + * Sends a post request to the admin_ajax.php, requesting posts based on the + * search term provided by the user. Defaults to all posts if no search term is + * provided. + * + * @since 2.7.0 + * + * @memberOf findPosts + * + * @return {void} + */ + send: function() { + var post = { + ps: $( '#find-posts-input' ).val(), + action: 'find_posts', + _ajax_nonce: $('#_ajax_nonce').val() + }, + spinner = $( '.find-box-search .spinner' ); + + spinner.addClass( 'is-active' ); + + /** + * Send a POST request to admin_ajax.php, hide the spinner and replace the list + * of posts with the response data. If an error occurs, display it. + */ + $.ajax( ajaxurl, { + type: 'POST', + data: post, + dataType: 'json' + }).always( function() { + spinner.removeClass( 'is-active' ); + }).done( function( x ) { + if ( ! x.success ) { + $( '#find-posts-response' ).text( wp.i18n.__( 'An error has occurred. Please reload the page and try again.' ) ); + } + + $( '#find-posts-response' ).html( x.data ); + }).fail( function() { + $( '#find-posts-response' ).text( wp.i18n.__( 'An error has occurred. Please reload the page and try again.' ) ); + }); + } + }; + + /** + * Initializes the file once the DOM is fully loaded and attaches events to the + * various form elements. + * + * @return {void} + */ + $( function() { + var settings, + $mediaGridWrap = $( '#wp-media-grid' ), + copyAttachmentURLClipboard = new ClipboardJS( '.copy-attachment-url.media-library' ), + copyAttachmentURLSuccessTimeout; + + // Opens a manage media frame into the grid. + if ( $mediaGridWrap.length && window.wp && window.wp.media ) { + settings = _wpMediaGridSettings; + + var frame = window.wp.media({ + frame: 'manage', + container: $mediaGridWrap, + library: settings.queryVars + }).open(); + + // Fire a global ready event. + $mediaGridWrap.trigger( 'wp-media-grid-ready', frame ); + } + + // Prevents form submission if no post has been selected. + $( '#find-posts-submit' ).on( 'click', function( event ) { + if ( ! $( '#find-posts-response input[type="radio"]:checked' ).length ) + event.preventDefault(); + }); + + // Submits the search query when hitting the enter key in the search input. + $( '#find-posts .find-box-search :input' ).on( 'keypress', function( event ) { + if ( 13 == event.which ) { + findPosts.send(); + return false; + } + }); + + // Binds the click event to the search button. + $( '#find-posts-search' ).on( 'click', findPosts.send ); + + // Binds the close dialog click event. + $( '#find-posts-close' ).on( 'click', findPosts.close ); + + // Binds the bulk action events to the submit buttons. + $( '#doaction' ).on( 'click', function( event ) { + + /* + * Handle the bulk action based on its value. + */ + $( 'select[name="action"]' ).each( function() { + var optionValue = $( this ).val(); + + if ( 'attach' === optionValue ) { + event.preventDefault(); + findPosts.open(); + } else if ( 'delete' === optionValue ) { + if ( ! showNotice.warn() ) { + event.preventDefault(); + } + } + }); + }); + + /** + * Enables clicking on the entire table row. + * + * @return {void} + */ + $( '.find-box-inside' ).on( 'click', 'tr', function() { + $( this ).find( '.found-radio input' ).prop( 'checked', true ); + }); + + /** + * Handles media list copy media URL button. + * + * @since 6.0.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(); + // Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680. + triggerElement.trigger( 'focus' ); + + // Show success visual feedback. + clearTimeout( copyAttachmentURLSuccessTimeout ); + successElement.removeClass( 'hidden' ); + + // Hide success visual feedback after 3 seconds since last success and unfocus the trigger. + copyAttachmentURLSuccessTimeout = setTimeout( function() { + successElement.addClass( 'hidden' ); + }, 3000 ); + + // Handle success audible feedback. + wp.a11y.speak( wp.i18n.__( 'The file URL has been copied to your clipboard' ) ); + } ); + }); +})( jQuery ); diff --git a/wp-admin/js/media.min.js b/wp-admin/js/media.min.js new file mode 100644 index 0000000..72eca61 --- /dev/null +++ b/wp-admin/js/media.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(s){window.findPosts={open:function(n,e){var i=s(".ui-find-overlay");return 0===i.length&&(s("body").append('<div class="ui-find-overlay"></div>'),findPosts.overlay()),i.show(),n&&e&&s("#affected").attr("name",n).val(e),s("#find-posts").show(),s("#find-posts-input").trigger("focus").on("keyup",function(n){27==n.which&&findPosts.close()}),findPosts.send(),!1},close:function(){s("#find-posts-response").empty(),s("#find-posts").hide(),s(".ui-find-overlay").hide()},overlay:function(){s(".ui-find-overlay").on("click",function(){findPosts.close()})},send:function(){var n={ps:s("#find-posts-input").val(),action:"find_posts",_ajax_nonce:s("#_ajax_nonce").val()},e=s(".find-box-search .spinner");e.addClass("is-active"),s.ajax(ajaxurl,{type:"POST",data:n,dataType:"json"}).always(function(){e.removeClass("is-active")}).done(function(n){n.success||s("#find-posts-response").text(wp.i18n.__("An error has occurred. Please reload the page and try again.")),s("#find-posts-response").html(n.data)}).fail(function(){s("#find-posts-response").text(wp.i18n.__("An error has occurred. Please reload the page and try again."))})}},s(function(){var o,n,e=s("#wp-media-grid"),i=new ClipboardJS(".copy-attachment-url.media-library");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)),s("#find-posts-submit").on("click",function(n){s('#find-posts-response input[type="radio"]:checked').length||n.preventDefault()}),s("#find-posts .find-box-search :input").on("keypress",function(n){if(13==n.which)return findPosts.send(),!1}),s("#find-posts-search").on("click",findPosts.send),s("#find-posts-close").on("click",findPosts.close),s("#doaction").on("click",function(e){s('select[name="action"]').each(function(){var n=s(this).val();"attach"===n?(e.preventDefault(),findPosts.open()):"delete"!==n||showNotice.warn()||e.preventDefault()})}),s(".find-box-inside").on("click","tr",function(){s(this).find(".found-radio input").prop("checked",!0)}),i.on("success",function(n){var e=s(n.trigger),i=s(".success",e.closest(".copy-to-clipboard-container"));n.clearSelection(),e.trigger("focus"),clearTimeout(o),i.removeClass("hidden"),o=setTimeout(function(){i.addClass("hidden")},3e3),wp.a11y.speak(wp.i18n.__("The file URL has been copied to your clipboard"))})})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/nav-menu.js b/wp-admin/js/nav-menu.js new file mode 100644 index 0000000..9877f78 --- /dev/null +++ b/wp-admin/js/nav-menu.js @@ -0,0 +1,1575 @@ +/** + * WordPress Administration Navigation Menu + * Interface JS functions + * + * @version 2.0.0 + * + * @package WordPress + * @subpackage Administration + * @output wp-admin/js/nav-menu.js + */ + +/* global menus, postboxes, columns, isRtl, ajaxurl, wpNavMenu */ + +(function($) { + + var api; + + /** + * Contains all the functions to handle WordPress navigation menus administration. + * + * @namespace wpNavMenu + */ + api = window.wpNavMenu = { + + options : { + menuItemDepthPerLevel : 30, // Do not use directly. Use depthToPx and pxToDepth instead. + globalMaxDepth: 11, + sortableItems: '> *', + targetTolerance: 0 + }, + + menuList : undefined, // Set in init. + targetList : undefined, // Set in init. + menusChanged : false, + isRTL: !! ( 'undefined' != typeof isRtl && isRtl ), + negateIfRTL: ( 'undefined' != typeof isRtl && isRtl ) ? -1 : 1, + lastSearch: '', + + // Functions that run on init. + init : function() { + api.menuList = $('#menu-to-edit'); + api.targetList = api.menuList; + + this.jQueryExtensions(); + + this.attachMenuEditListeners(); + + this.attachBulkSelectButtonListeners(); + this.attachMenuCheckBoxListeners(); + this.attachMenuItemDeleteButton(); + this.attachPendingMenuItemsListForDeletion(); + + this.attachQuickSearchListeners(); + this.attachThemeLocationsListeners(); + this.attachMenuSaveSubmitListeners(); + + this.attachTabsPanelListeners(); + + this.attachUnsavedChangesListener(); + + if ( api.menuList.length ) + this.initSortables(); + + if ( menus.oneThemeLocationNoMenus ) + $( '#posttype-page' ).addSelectedToMenu( api.addMenuItemToBottom ); + + this.initManageLocations(); + + this.initAccessibility(); + + this.initToggles(); + + this.initPreviewing(); + }, + + jQueryExtensions : function() { + // jQuery extensions. + $.fn.extend({ + menuItemDepth : function() { + var margin = api.isRTL ? this.eq(0).css('margin-right') : this.eq(0).css('margin-left'); + return api.pxToDepth( margin && -1 != margin.indexOf('px') ? margin.slice(0, -2) : 0 ); + }, + updateDepthClass : function(current, prev) { + return this.each(function(){ + var t = $(this); + prev = prev || t.menuItemDepth(); + $(this).removeClass('menu-item-depth-'+ prev ) + .addClass('menu-item-depth-'+ current ); + }); + }, + shiftDepthClass : function(change) { + return this.each(function(){ + var t = $(this), + depth = t.menuItemDepth(), + newDepth = depth + change; + + t.removeClass( 'menu-item-depth-'+ depth ) + .addClass( 'menu-item-depth-'+ ( newDepth ) ); + + if ( 0 === newDepth ) { + t.find( '.is-submenu' ).hide(); + } + }); + }, + childMenuItems : function() { + var result = $(); + this.each(function(){ + var t = $(this), depth = t.menuItemDepth(), next = t.next( '.menu-item' ); + while( next.length && next.menuItemDepth() > depth ) { + result = result.add( next ); + next = next.next( '.menu-item' ); + } + }); + return result; + }, + shiftHorizontally : function( dir ) { + return this.each(function(){ + var t = $(this), + depth = t.menuItemDepth(), + newDepth = depth + dir; + + // Change .menu-item-depth-n class. + t.moveHorizontally( newDepth, depth ); + }); + }, + moveHorizontally : function( newDepth, depth ) { + return this.each(function(){ + var t = $(this), + children = t.childMenuItems(), + diff = newDepth - depth, + subItemText = t.find('.is-submenu'); + + // Change .menu-item-depth-n class. + t.updateDepthClass( newDepth, depth ).updateParentMenuItemDBId(); + + // If it has children, move those too. + if ( children ) { + children.each(function() { + var t = $(this), + thisDepth = t.menuItemDepth(), + newDepth = thisDepth + diff; + t.updateDepthClass(newDepth, thisDepth).updateParentMenuItemDBId(); + }); + } + + // Show "Sub item" helper text. + if (0 === newDepth) + subItemText.hide(); + else + subItemText.show(); + }); + }, + updateParentMenuItemDBId : function() { + return this.each(function(){ + var item = $(this), + input = item.find( '.menu-item-data-parent-id' ), + depth = parseInt( item.menuItemDepth(), 10 ), + parentDepth = depth - 1, + parent = item.prevAll( '.menu-item-depth-' + parentDepth ).first(); + + if ( 0 === depth ) { // Item is on the top level, has no parent. + input.val(0); + } else { // Find the parent item, and retrieve its object id. + input.val( parent.find( '.menu-item-data-db-id' ).val() ); + } + }); + }, + hideAdvancedMenuItemFields : function() { + return this.each(function(){ + var that = $(this); + $('.hide-column-tog').not(':checked').each(function(){ + that.find('.field-' + $(this).val() ).addClass('hidden-field'); + }); + }); + }, + /** + * Adds selected menu items to the menu. + * + * @ignore + * + * @param jQuery metabox The metabox jQuery object. + */ + addSelectedToMenu : function(processMethod) { + if ( 0 === $('#menu-to-edit').length ) { + return false; + } + + return this.each(function() { + var t = $(this), menuItems = {}, + checkboxes = ( menus.oneThemeLocationNoMenus && 0 === t.find( '.tabs-panel-active .categorychecklist li input:checked' ).length ) ? t.find( '#page-all li input[type="checkbox"]' ) : t.find( '.tabs-panel-active .categorychecklist li input:checked' ), + re = /menu-item\[([^\]]*)/; + + processMethod = processMethod || api.addMenuItemToBottom; + + // If no items are checked, bail. + if ( !checkboxes.length ) + return false; + + // Show the Ajax spinner. + t.find( '.button-controls .spinner' ).addClass( 'is-active' ); + + // Retrieve menu item data. + $(checkboxes).each(function(){ + var t = $(this), + listItemDBIDMatch = re.exec( t.attr('name') ), + listItemDBID = 'undefined' == typeof listItemDBIDMatch[1] ? 0 : parseInt(listItemDBIDMatch[1], 10); + + if ( this.className && -1 != this.className.indexOf('add-to-top') ) + processMethod = api.addMenuItemToTop; + menuItems[listItemDBID] = t.closest('li').getItemData( 'add-menu-item', listItemDBID ); + }); + + // Add the items. + api.addItemToMenu(menuItems, processMethod, function(){ + // Deselect the items and hide the Ajax spinner. + checkboxes.prop( 'checked', false ); + t.find( '.button-controls .select-all' ).prop( 'checked', false ); + t.find( '.button-controls .spinner' ).removeClass( 'is-active' ); + }); + }); + }, + getItemData : function( itemType, id ) { + itemType = itemType || 'menu-item'; + + var itemData = {}, i, + fields = [ + 'menu-item-db-id', + 'menu-item-object-id', + 'menu-item-object', + 'menu-item-parent-id', + 'menu-item-position', + 'menu-item-type', + 'menu-item-title', + 'menu-item-url', + 'menu-item-description', + 'menu-item-attr-title', + 'menu-item-target', + 'menu-item-classes', + 'menu-item-xfn' + ]; + + if( !id && itemType == 'menu-item' ) { + id = this.find('.menu-item-data-db-id').val(); + } + + if( !id ) return itemData; + + this.find('input').each(function() { + var field; + i = fields.length; + while ( i-- ) { + if( itemType == 'menu-item' ) + field = fields[i] + '[' + id + ']'; + else if( itemType == 'add-menu-item' ) + field = 'menu-item[' + id + '][' + fields[i] + ']'; + + if ( + this.name && + field == this.name + ) { + itemData[fields[i]] = this.value; + } + } + }); + + return itemData; + }, + setItemData : function( itemData, itemType, id ) { // Can take a type, such as 'menu-item', or an id. + itemType = itemType || 'menu-item'; + + if( !id && itemType == 'menu-item' ) { + id = $('.menu-item-data-db-id', this).val(); + } + + if( !id ) return this; + + this.find('input').each(function() { + var t = $(this), field; + $.each( itemData, function( attr, val ) { + if( itemType == 'menu-item' ) + field = attr + '[' + id + ']'; + else if( itemType == 'add-menu-item' ) + field = 'menu-item[' + id + '][' + attr + ']'; + + if ( field == t.attr('name') ) { + t.val( val ); + } + }); + }); + return this; + } + }); + }, + + countMenuItems : function( depth ) { + return $( '.menu-item-depth-' + depth ).length; + }, + + moveMenuItem : function( $this, dir ) { + + var items, newItemPosition, newDepth, + menuItems = $( '#menu-to-edit li' ), + menuItemsCount = menuItems.length, + thisItem = $this.parents( 'li.menu-item' ), + thisItemChildren = thisItem.childMenuItems(), + thisItemData = thisItem.getItemData(), + thisItemDepth = parseInt( thisItem.menuItemDepth(), 10 ), + thisItemPosition = parseInt( thisItem.index(), 10 ), + nextItem = thisItem.next(), + nextItemChildren = nextItem.childMenuItems(), + nextItemDepth = parseInt( nextItem.menuItemDepth(), 10 ) + 1, + prevItem = thisItem.prev(), + prevItemDepth = parseInt( prevItem.menuItemDepth(), 10 ), + prevItemId = prevItem.getItemData()['menu-item-db-id'], + a11ySpeech = menus[ 'moved' + dir.charAt(0).toUpperCase() + dir.slice(1) ]; + + switch ( dir ) { + case 'up': + newItemPosition = thisItemPosition - 1; + + // Already at top. + if ( 0 === thisItemPosition ) + break; + + // If a sub item is moved to top, shift it to 0 depth. + if ( 0 === newItemPosition && 0 !== thisItemDepth ) + thisItem.moveHorizontally( 0, thisItemDepth ); + + // If prev item is sub item, shift to match depth. + if ( 0 !== prevItemDepth ) + thisItem.moveHorizontally( prevItemDepth, thisItemDepth ); + + // Does this item have sub items? + if ( thisItemChildren ) { + items = thisItem.add( thisItemChildren ); + // Move the entire block. + items.detach().insertBefore( menuItems.eq( newItemPosition ) ).updateParentMenuItemDBId(); + } else { + thisItem.detach().insertBefore( menuItems.eq( newItemPosition ) ).updateParentMenuItemDBId(); + } + break; + case 'down': + // Does this item have sub items? + if ( thisItemChildren ) { + items = thisItem.add( thisItemChildren ), + nextItem = menuItems.eq( items.length + thisItemPosition ), + nextItemChildren = 0 !== nextItem.childMenuItems().length; + + if ( nextItemChildren ) { + newDepth = parseInt( nextItem.menuItemDepth(), 10 ) + 1; + thisItem.moveHorizontally( newDepth, thisItemDepth ); + } + + // Have we reached the bottom? + if ( menuItemsCount === thisItemPosition + items.length ) + break; + + items.detach().insertAfter( menuItems.eq( thisItemPosition + items.length ) ).updateParentMenuItemDBId(); + } else { + // If next item has sub items, shift depth. + if ( 0 !== nextItemChildren.length ) + thisItem.moveHorizontally( nextItemDepth, thisItemDepth ); + + // Have we reached the bottom? + if ( menuItemsCount === thisItemPosition + 1 ) + break; + thisItem.detach().insertAfter( menuItems.eq( thisItemPosition + 1 ) ).updateParentMenuItemDBId(); + } + break; + case 'top': + // Already at top. + if ( 0 === thisItemPosition ) + break; + // Does this item have sub items? + if ( thisItemChildren ) { + items = thisItem.add( thisItemChildren ); + // Move the entire block. + items.detach().insertBefore( menuItems.eq( 0 ) ).updateParentMenuItemDBId(); + } else { + thisItem.detach().insertBefore( menuItems.eq( 0 ) ).updateParentMenuItemDBId(); + } + break; + case 'left': + // As far left as possible. + if ( 0 === thisItemDepth ) + break; + thisItem.shiftHorizontally( -1 ); + break; + case 'right': + // Can't be sub item at top. + if ( 0 === thisItemPosition ) + break; + // Already sub item of prevItem. + if ( thisItemData['menu-item-parent-id'] === prevItemId ) + break; + thisItem.shiftHorizontally( 1 ); + break; + } + $this.trigger( 'focus' ); + api.registerChange(); + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + + if ( a11ySpeech ) { + wp.a11y.speak( a11ySpeech ); + } + }, + + initAccessibility : function() { + var menu = $( '#menu-to-edit' ); + + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + + // Refresh the accessibility when the user comes close to the item in any way. + menu.on( 'mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility' , '.menu-item' , function(){ + api.refreshAdvancedAccessibilityOfItem( $( this ).find( 'a.item-edit' ) ); + } ); + + // We have to update on click as well because we might hover first, change the item, and then click. + menu.on( 'click', 'a.item-edit', function() { + api.refreshAdvancedAccessibilityOfItem( $( this ) ); + } ); + + // Links for moving items. + menu.on( 'click', '.menus-move', function () { + var $this = $( this ), + dir = $this.data( 'dir' ); + + if ( 'undefined' !== typeof dir ) { + api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), dir ); + } + }); + }, + + /** + * refreshAdvancedAccessibilityOfItem( [itemToRefresh] ) + * + * Refreshes advanced accessibility buttons for one menu item. + * Shows or hides buttons based on the location of the menu item. + * + * @param {Object} itemToRefresh The menu item that might need its advanced accessibility buttons refreshed + */ + refreshAdvancedAccessibilityOfItem : function( itemToRefresh ) { + + // Only refresh accessibility when necessary. + if ( true !== $( itemToRefresh ).data( 'needs_accessibility_refresh' ) ) { + return; + } + + var thisLink, thisLinkText, primaryItems, itemPosition, title, + parentItem, parentItemId, parentItemName, subItems, + $this = $( itemToRefresh ), + menuItem = $this.closest( 'li.menu-item' ).first(), + depth = menuItem.menuItemDepth(), + isPrimaryMenuItem = ( 0 === depth ), + itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(), + position = parseInt( menuItem.index(), 10 ), + prevItemDepth = ( isPrimaryMenuItem ) ? depth : parseInt( depth - 1, 10 ), + prevItemNameLeft = menuItem.prevAll('.menu-item-depth-' + prevItemDepth).first().find( '.menu-item-title' ).text(), + prevItemNameRight = menuItem.prevAll('.menu-item-depth-' + depth).first().find( '.menu-item-title' ).text(), + totalMenuItems = $('#menu-to-edit li').length, + hasSameDepthSibling = menuItem.nextAll( '.menu-item-depth-' + depth ).length; + + menuItem.find( '.field-move' ).toggle( totalMenuItems > 1 ); + + // Where can they move this menu item? + if ( 0 !== position ) { + thisLink = menuItem.find( '.menus-move-up' ); + thisLink.attr( 'aria-label', menus.moveUp ).css( 'display', 'inline' ); + } + + if ( 0 !== position && isPrimaryMenuItem ) { + thisLink = menuItem.find( '.menus-move-top' ); + thisLink.attr( 'aria-label', menus.moveToTop ).css( 'display', 'inline' ); + } + + if ( position + 1 !== totalMenuItems && 0 !== position ) { + thisLink = menuItem.find( '.menus-move-down' ); + thisLink.attr( 'aria-label', menus.moveDown ).css( 'display', 'inline' ); + } + + if ( 0 === position && 0 !== hasSameDepthSibling ) { + thisLink = menuItem.find( '.menus-move-down' ); + thisLink.attr( 'aria-label', menus.moveDown ).css( 'display', 'inline' ); + } + + if ( ! isPrimaryMenuItem ) { + thisLink = menuItem.find( '.menus-move-left' ), + thisLinkText = menus.outFrom.replace( '%s', prevItemNameLeft ); + thisLink.attr( 'aria-label', menus.moveOutFrom.replace( '%s', prevItemNameLeft ) ).text( thisLinkText ).css( 'display', 'inline' ); + } + + if ( 0 !== position ) { + if ( menuItem.find( '.menu-item-data-parent-id' ).val() !== menuItem.prev().find( '.menu-item-data-db-id' ).val() ) { + thisLink = menuItem.find( '.menus-move-right' ), + thisLinkText = menus.under.replace( '%s', prevItemNameRight ); + thisLink.attr( 'aria-label', menus.moveUnder.replace( '%s', prevItemNameRight ) ).text( thisLinkText ).css( 'display', 'inline' ); + } + } + + if ( isPrimaryMenuItem ) { + primaryItems = $( '.menu-item-depth-0' ), + itemPosition = primaryItems.index( menuItem ) + 1, + totalMenuItems = primaryItems.length, + + // String together help text for primary menu items. + title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$d', totalMenuItems ); + } else { + parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(), + parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(), + parentItemName = parentItem.find( '.menu-item-title' ).text(), + subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ), + itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1; + + // String together help text for sub menu items. + title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$d', itemPosition ).replace( '%3$s', parentItemName ); + } + + $this.attr( 'aria-label', title ); + + // Mark this item's accessibility as refreshed. + $this.data( 'needs_accessibility_refresh', false ); + }, + + /** + * refreshAdvancedAccessibility + * + * Hides all advanced accessibility buttons and marks them for refreshing. + */ + refreshAdvancedAccessibility : function() { + + // Hide all the move buttons by default. + $( '.menu-item-settings .field-move .menus-move' ).hide(); + + // Mark all menu items as unprocessed. + $( 'a.item-edit' ).data( 'needs_accessibility_refresh', true ); + + // All open items have to be refreshed or they will show no links. + $( '.menu-item-edit-active a.item-edit' ).each( function() { + api.refreshAdvancedAccessibilityOfItem( this ); + } ); + }, + + refreshKeyboardAccessibility : function() { + $( 'a.item-edit' ).off( 'focus' ).on( 'focus', function(){ + $(this).off( 'keydown' ).on( 'keydown', function(e){ + + var arrows, + $this = $( this ), + thisItem = $this.parents( 'li.menu-item' ), + thisItemData = thisItem.getItemData(); + + // Bail if it's not an arrow key. + if ( 37 != e.which && 38 != e.which && 39 != e.which && 40 != e.which ) + return; + + // Avoid multiple keydown events. + $this.off('keydown'); + + // Bail if there is only one menu item. + if ( 1 === $('#menu-to-edit li').length ) + return; + + // If RTL, swap left/right arrows. + arrows = { '38': 'up', '40': 'down', '37': 'left', '39': 'right' }; + if ( $('body').hasClass('rtl') ) + arrows = { '38' : 'up', '40' : 'down', '39' : 'left', '37' : 'right' }; + + switch ( arrows[e.which] ) { + case 'up': + api.moveMenuItem( $this, 'up' ); + break; + case 'down': + api.moveMenuItem( $this, 'down' ); + break; + case 'left': + api.moveMenuItem( $this, 'left' ); + break; + case 'right': + api.moveMenuItem( $this, 'right' ); + break; + } + // Put focus back on same menu item. + $( '#edit-' + thisItemData['menu-item-db-id'] ).trigger( 'focus' ); + return false; + }); + }); + }, + + initPreviewing : function() { + // Update the item handle title when the navigation label is changed. + $( '#menu-to-edit' ).on( 'change input', '.edit-menu-item-title', function(e) { + var input = $( e.currentTarget ), title, titleEl; + title = input.val(); + titleEl = input.closest( '.menu-item' ).find( '.menu-item-title' ); + // Don't update to empty title. + if ( title ) { + titleEl.text( title ).removeClass( 'no-title' ); + } else { + titleEl.text( wp.i18n._x( '(no label)', 'missing menu item navigation label' ) ).addClass( 'no-title' ); + } + } ); + }, + + initToggles : function() { + // Init postboxes. + postboxes.add_postbox_toggles('nav-menus'); + + // Adjust columns functions for menus UI. + columns.useCheckboxesForHidden(); + columns.checked = function(field) { + $('.field-' + field).removeClass('hidden-field'); + }; + columns.unchecked = function(field) { + $('.field-' + field).addClass('hidden-field'); + }; + // Hide fields. + api.menuList.hideAdvancedMenuItemFields(); + + $('.hide-postbox-tog').on( 'click', function () { + var hidden = $( '.accordion-container li.accordion-section' ).filter(':hidden').map(function() { return this.id; }).get().join(','); + $.post(ajaxurl, { + action: 'closed-postboxes', + hidden: hidden, + closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(), + page: 'nav-menus' + }); + }); + }, + + initSortables : function() { + var currentDepth = 0, originalDepth, minDepth, maxDepth, + prev, next, prevBottom, nextThreshold, helperHeight, transport, + menuEdge = api.menuList.offset().left, + body = $('body'), maxChildDepth, + menuMaxDepth = initialMenuMaxDepth(); + + if( 0 !== $( '#menu-to-edit li' ).length ) + $( '.drag-instructions' ).show(); + + // Use the right edge if RTL. + menuEdge += api.isRTL ? api.menuList.width() : 0; + + api.menuList.sortable({ + handle: '.menu-item-handle', + placeholder: 'sortable-placeholder', + items: api.options.sortableItems, + start: function(e, ui) { + var height, width, parent, children, tempHolder; + + // Handle placement for RTL orientation. + if ( api.isRTL ) + ui.item[0].style.right = 'auto'; + + transport = ui.item.children('.menu-item-transport'); + + // Set depths. currentDepth must be set before children are located. + originalDepth = ui.item.menuItemDepth(); + updateCurrentDepth(ui, originalDepth); + + // Attach child elements to parent. + // Skip the placeholder. + parent = ( ui.item.next()[0] == ui.placeholder[0] ) ? ui.item.next() : ui.item; + children = parent.childMenuItems(); + transport.append( children ); + + // Update the height of the placeholder to match the moving item. + height = transport.outerHeight(); + // If there are children, account for distance between top of children and parent. + height += ( height > 0 ) ? (ui.placeholder.css('margin-top').slice(0, -2) * 1) : 0; + height += ui.helper.outerHeight(); + helperHeight = height; + height -= 2; // Subtract 2 for borders. + ui.placeholder.height(height); + + // Update the width of the placeholder to match the moving item. + maxChildDepth = originalDepth; + children.each(function(){ + var depth = $(this).menuItemDepth(); + maxChildDepth = (depth > maxChildDepth) ? depth : maxChildDepth; + }); + width = ui.helper.find('.menu-item-handle').outerWidth(); // Get original width. + width += api.depthToPx(maxChildDepth - originalDepth); // Account for children. + width -= 2; // Subtract 2 for borders. + ui.placeholder.width(width); + + // Update the list of menu items. + tempHolder = ui.placeholder.next( '.menu-item' ); + tempHolder.css( 'margin-top', helperHeight + 'px' ); // Set the margin to absorb the placeholder. + ui.placeholder.detach(); // Detach or jQuery UI will think the placeholder is a menu item. + $(this).sortable( 'refresh' ); // The children aren't sortable. We should let jQuery UI know. + ui.item.after( ui.placeholder ); // Reattach the placeholder. + tempHolder.css('margin-top', 0); // Reset the margin. + + // Now that the element is complete, we can update... + updateSharedVars(ui); + }, + stop: function(e, ui) { + var children, subMenuTitle, + depthChange = currentDepth - originalDepth; + + // Return child elements to the list. + children = transport.children().insertAfter(ui.item); + + // Add "sub menu" description. + subMenuTitle = ui.item.find( '.item-title .is-submenu' ); + if ( 0 < currentDepth ) + subMenuTitle.show(); + else + subMenuTitle.hide(); + + // Update depth classes. + if ( 0 !== depthChange ) { + ui.item.updateDepthClass( currentDepth ); + children.shiftDepthClass( depthChange ); + updateMenuMaxDepth( depthChange ); + } + // Register a change. + api.registerChange(); + // Update the item data. + ui.item.updateParentMenuItemDBId(); + + // Address sortable's incorrectly-calculated top in Opera. + ui.item[0].style.top = 0; + + // Handle drop placement for rtl orientation. + if ( api.isRTL ) { + ui.item[0].style.left = 'auto'; + ui.item[0].style.right = 0; + } + + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + }, + change: function(e, ui) { + // Make sure the placeholder is inside the menu. + // Otherwise fix it, or we're in trouble. + if( ! ui.placeholder.parent().hasClass('menu') ) + (prev.length) ? prev.after( ui.placeholder ) : api.menuList.prepend( ui.placeholder ); + + updateSharedVars(ui); + }, + sort: function(e, ui) { + var offset = ui.helper.offset(), + edge = api.isRTL ? offset.left + ui.helper.width() : offset.left, + depth = api.negateIfRTL * api.pxToDepth( edge - menuEdge ); + + /* + * Check and correct if depth is not within range. + * Also, if the dragged element is dragged upwards over an item, + * shift the placeholder to a child position. + */ + if ( depth > maxDepth || offset.top < ( prevBottom - api.options.targetTolerance ) ) { + depth = maxDepth; + } else if ( depth < minDepth ) { + depth = minDepth; + } + + if( depth != currentDepth ) + updateCurrentDepth(ui, depth); + + // If we overlap the next element, manually shift downwards. + if( nextThreshold && offset.top + helperHeight > nextThreshold ) { + next.after( ui.placeholder ); + updateSharedVars( ui ); + $( this ).sortable( 'refreshPositions' ); + } + } + }); + + function updateSharedVars(ui) { + var depth; + + prev = ui.placeholder.prev( '.menu-item' ); + next = ui.placeholder.next( '.menu-item' ); + + // Make sure we don't select the moving item. + if( prev[0] == ui.item[0] ) prev = prev.prev( '.menu-item' ); + if( next[0] == ui.item[0] ) next = next.next( '.menu-item' ); + + prevBottom = (prev.length) ? prev.offset().top + prev.height() : 0; + nextThreshold = (next.length) ? next.offset().top + next.height() / 3 : 0; + minDepth = (next.length) ? next.menuItemDepth() : 0; + + if( prev.length ) + maxDepth = ( (depth = prev.menuItemDepth() + 1) > api.options.globalMaxDepth ) ? api.options.globalMaxDepth : depth; + else + maxDepth = 0; + } + + function updateCurrentDepth(ui, depth) { + ui.placeholder.updateDepthClass( depth, currentDepth ); + currentDepth = depth; + } + + function initialMenuMaxDepth() { + if( ! body[0].className ) return 0; + var match = body[0].className.match(/menu-max-depth-(\d+)/); + return match && match[1] ? parseInt( match[1], 10 ) : 0; + } + + function updateMenuMaxDepth( depthChange ) { + var depth, newDepth = menuMaxDepth; + if ( depthChange === 0 ) { + return; + } else if ( depthChange > 0 ) { + depth = maxChildDepth + depthChange; + if( depth > menuMaxDepth ) + newDepth = depth; + } else if ( depthChange < 0 && maxChildDepth == menuMaxDepth ) { + while( ! $('.menu-item-depth-' + newDepth, api.menuList).length && newDepth > 0 ) + newDepth--; + } + // Update the depth class. + body.removeClass( 'menu-max-depth-' + menuMaxDepth ).addClass( 'menu-max-depth-' + newDepth ); + menuMaxDepth = newDepth; + } + }, + + initManageLocations : function () { + $('#menu-locations-wrap form').on( 'submit', function(){ + window.onbeforeunload = null; + }); + $('.menu-location-menus select').on('change', function () { + var editLink = $(this).closest('tr').find('.locations-edit-menu-link'); + if ($(this).find('option:selected').data('orig')) + editLink.show(); + else + editLink.hide(); + }); + }, + + attachMenuEditListeners : function() { + var that = this; + $('#update-nav-menu').on('click', function(e) { + if ( e.target && e.target.className ) { + if ( -1 != e.target.className.indexOf('item-edit') ) { + return that.eventOnClickEditLink(e.target); + } else if ( -1 != e.target.className.indexOf('menu-save') ) { + return that.eventOnClickMenuSave(e.target); + } else if ( -1 != e.target.className.indexOf('menu-delete') ) { + return that.eventOnClickMenuDelete(e.target); + } else if ( -1 != e.target.className.indexOf('item-delete') ) { + return that.eventOnClickMenuItemDelete(e.target); + } else if ( -1 != e.target.className.indexOf('item-cancel') ) { + return that.eventOnClickCancelLink(e.target); + } + } + }); + + $( '#menu-name' ).on( 'input', _.debounce( function () { + var menuName = $( document.getElementById( 'menu-name' ) ), + menuNameVal = menuName.val(); + + if ( ! menuNameVal || ! menuNameVal.replace( /\s+/, '' ) ) { + // Add warning for invalid menu name. + menuName.parent().addClass( 'form-invalid' ); + } else { + // Remove warning for valid menu name. + menuName.parent().removeClass( 'form-invalid' ); + } + }, 500 ) ); + + $('#add-custom-links input[type="text"]').on( 'keypress', function(e){ + $('#customlinkdiv').removeClass('form-invalid'); + + if ( e.keyCode === 13 ) { + e.preventDefault(); + $( '#submit-customlinkdiv' ).trigger( 'click' ); + } + }); + }, + + /** + * Handle toggling bulk selection checkboxes for menu items. + * + * @since 5.8.0 + */ + attachBulkSelectButtonListeners : function() { + var that = this; + + $( '.bulk-select-switcher' ).on( 'change', function() { + if ( this.checked ) { + $( '.bulk-select-switcher' ).prop( 'checked', true ); + that.enableBulkSelection(); + } else { + $( '.bulk-select-switcher' ).prop( 'checked', false ); + that.disableBulkSelection(); + } + }); + }, + + /** + * Enable bulk selection checkboxes for menu items. + * + * @since 5.8.0 + */ + enableBulkSelection : function() { + var checkbox = $( '#menu-to-edit .menu-item-checkbox' ); + + $( '#menu-to-edit' ).addClass( 'bulk-selection' ); + $( '#nav-menu-bulk-actions-top' ).addClass( 'bulk-selection' ); + $( '#nav-menu-bulk-actions-bottom' ).addClass( 'bulk-selection' ); + + $.each( checkbox, function() { + $(this).prop( 'disabled', false ); + }); + }, + + /** + * Disable bulk selection checkboxes for menu items. + * + * @since 5.8.0 + */ + disableBulkSelection : function() { + var checkbox = $( '#menu-to-edit .menu-item-checkbox' ); + + $( '#menu-to-edit' ).removeClass( 'bulk-selection' ); + $( '#nav-menu-bulk-actions-top' ).removeClass( 'bulk-selection' ); + $( '#nav-menu-bulk-actions-bottom' ).removeClass( 'bulk-selection' ); + + if ( $( '.menu-items-delete' ).is( '[aria-describedby="pending-menu-items-to-delete"]' ) ) { + $( '.menu-items-delete' ).removeAttr( 'aria-describedby' ); + } + + $.each( checkbox, function() { + $(this).prop( 'disabled', true ).prop( 'checked', false ); + }); + + $( '.menu-items-delete' ).addClass( 'disabled' ); + $( '#pending-menu-items-to-delete ul' ).empty(); + }, + + /** + * Listen for state changes on bulk action checkboxes. + * + * @since 5.8.0 + */ + attachMenuCheckBoxListeners : function() { + var that = this; + + $( '#menu-to-edit' ).on( 'change', '.menu-item-checkbox', function() { + that.setRemoveSelectedButtonStatus(); + }); + }, + + /** + * Create delete button to remove menu items from collection. + * + * @since 5.8.0 + */ + attachMenuItemDeleteButton : function() { + var that = this; + + $( document ).on( 'click', '.menu-items-delete', function( e ) { + var itemsPendingDeletion, itemsPendingDeletionList, deletionSpeech; + + e.preventDefault(); + + if ( ! $(this).hasClass( 'disabled' ) ) { + $.each( $( '.menu-item-checkbox:checked' ), function( index, element ) { + $( element ).parents( 'li' ).find( 'a.item-delete' ).trigger( 'click' ); + }); + + $( '.menu-items-delete' ).addClass( 'disabled' ); + $( '.bulk-select-switcher' ).prop( 'checked', false ); + + itemsPendingDeletion = ''; + itemsPendingDeletionList = $( '#pending-menu-items-to-delete ul li' ); + + $.each( itemsPendingDeletionList, function( index, element ) { + var itemName = $( element ).find( '.pending-menu-item-name' ).text(); + var itemSpeech = menus.menuItemDeletion.replace( '%s', itemName ); + + itemsPendingDeletion += itemSpeech; + if ( ( index + 1 ) < itemsPendingDeletionList.length ) { + itemsPendingDeletion += ', '; + } + }); + + deletionSpeech = menus.itemsDeleted.replace( '%s', itemsPendingDeletion ); + wp.a11y.speak( deletionSpeech, 'polite' ); + that.disableBulkSelection(); + } + }); + }, + + /** + * List menu items awaiting deletion. + * + * @since 5.8.0 + */ + attachPendingMenuItemsListForDeletion : function() { + $( '#post-body-content' ).on( 'change', '.menu-item-checkbox', function() { + var menuItemName, menuItemType, menuItemID, listedMenuItem; + + if ( ! $( '.menu-items-delete' ).is( '[aria-describedby="pending-menu-items-to-delete"]' ) ) { + $( '.menu-items-delete' ).attr( 'aria-describedby', 'pending-menu-items-to-delete' ); + } + + menuItemName = $(this).next().text(); + menuItemType = $(this).parent().next( '.item-controls' ).find( '.item-type' ).text(); + menuItemID = $(this).attr( 'data-menu-item-id' ); + + listedMenuItem = $( '#pending-menu-items-to-delete ul' ).find( '[data-menu-item-id=' + menuItemID + ']' ); + if ( listedMenuItem.length > 0 ) { + listedMenuItem.remove(); + } + + if ( this.checked === true ) { + $( '#pending-menu-items-to-delete ul' ).append( + '<li data-menu-item-id="' + menuItemID + '">' + + '<span class="pending-menu-item-name">' + menuItemName + '</span> ' + + '<span class="pending-menu-item-type">(' + menuItemType + ')</span>' + + '<span class="separator"></span>' + + '</li>' + ); + } + + $( '#pending-menu-items-to-delete li .separator' ).html( ', ' ); + $( '#pending-menu-items-to-delete li .separator' ).last().html( '.' ); + }); + }, + + /** + * Set status of bulk delete checkbox. + * + * @since 5.8.0 + */ + setBulkDeleteCheckboxStatus : function() { + var that = this; + var checkbox = $( '#menu-to-edit .menu-item-checkbox' ); + + $.each( checkbox, function() { + if ( $(this).prop( 'disabled' ) ) { + $(this).prop( 'disabled', false ); + } else { + $(this).prop( 'disabled', true ); + } + + if ( $(this).is( ':checked' ) ) { + $(this).prop( 'checked', false ); + } + }); + + that.setRemoveSelectedButtonStatus(); + }, + + /** + * Set status of menu items removal button. + * + * @since 5.8.0 + */ + setRemoveSelectedButtonStatus : function() { + var button = $( '.menu-items-delete' ); + + if ( $( '.menu-item-checkbox:checked' ).length > 0 ) { + button.removeClass( 'disabled' ); + } else { + button.addClass( 'disabled' ); + } + }, + + attachMenuSaveSubmitListeners : function() { + /* + * When a navigation menu is saved, store a JSON representation of all form data + * in a single input to avoid PHP `max_input_vars` limitations. See #14134. + */ + $( '#update-nav-menu' ).on( 'submit', function() { + var navMenuData = $( '#update-nav-menu' ).serializeArray(); + $( '[name="nav-menu-data"]' ).val( JSON.stringify( navMenuData ) ); + }); + }, + + attachThemeLocationsListeners : function() { + var loc = $('#nav-menu-theme-locations'), params = {}; + params.action = 'menu-locations-save'; + params['menu-settings-column-nonce'] = $('#menu-settings-column-nonce').val(); + loc.find('input[type="submit"]').on( 'click', function() { + loc.find('select').each(function() { + params[this.name] = $(this).val(); + }); + loc.find( '.spinner' ).addClass( 'is-active' ); + $.post( ajaxurl, params, function() { + loc.find( '.spinner' ).removeClass( 'is-active' ); + }); + return false; + }); + }, + + attachQuickSearchListeners : function() { + var searchTimer; + + // Prevent form submission. + $( '#nav-menu-meta' ).on( 'submit', function( event ) { + event.preventDefault(); + }); + + $( '#nav-menu-meta' ).on( 'input', '.quick-search', function() { + var $this = $( this ); + + $this.attr( 'autocomplete', 'off' ); + + if ( searchTimer ) { + clearTimeout( searchTimer ); + } + + searchTimer = setTimeout( function() { + api.updateQuickSearchResults( $this ); + }, 500 ); + }).on( 'blur', '.quick-search', function() { + api.lastSearch = ''; + }); + }, + + updateQuickSearchResults : function(input) { + var panel, params, + minSearchLength = 2, + q = input.val(); + + /* + * Minimum characters for a search. Also avoid a new Ajax search when + * the pressed key (e.g. arrows) doesn't change the searched term. + */ + if ( q.length < minSearchLength || api.lastSearch == q ) { + return; + } + + api.lastSearch = q; + + panel = input.parents('.tabs-panel'); + params = { + 'action': 'menu-quick-search', + 'response-format': 'markup', + 'menu': $('#menu').val(), + 'menu-settings-column-nonce': $('#menu-settings-column-nonce').val(), + 'q': q, + 'type': input.attr('name') + }; + + $( '.spinner', panel ).addClass( 'is-active' ); + + $.post( ajaxurl, params, function(menuMarkup) { + api.processQuickSearchQueryResponse(menuMarkup, params, panel); + }); + }, + + addCustomLink : function( processMethod ) { + var url = $('#custom-menu-item-url').val().toString(), + label = $('#custom-menu-item-name').val(); + + if ( '' !== url ) { + url = url.trim(); + } + + processMethod = processMethod || api.addMenuItemToBottom; + + if ( '' === url || 'https://' == url || 'http://' == url ) { + $('#customlinkdiv').addClass('form-invalid'); + return false; + } + + // Show the Ajax spinner. + $( '.customlinkdiv .spinner' ).addClass( 'is-active' ); + this.addLinkToMenu( url, label, processMethod, function() { + // Remove the Ajax spinner. + $( '.customlinkdiv .spinner' ).removeClass( 'is-active' ); + // Set custom link form back to defaults. + $('#custom-menu-item-name').val('').trigger( 'blur' ); + $( '#custom-menu-item-url' ).val( '' ).attr( 'placeholder', 'https://' ); + }); + }, + + addLinkToMenu : function(url, label, processMethod, callback) { + processMethod = processMethod || api.addMenuItemToBottom; + callback = callback || function(){}; + + api.addItemToMenu({ + '-1': { + 'menu-item-type': 'custom', + 'menu-item-url': url, + 'menu-item-title': label + } + }, processMethod, callback); + }, + + addItemToMenu : function(menuItem, processMethod, callback) { + var menu = $('#menu').val(), + nonce = $('#menu-settings-column-nonce').val(), + params; + + processMethod = processMethod || function(){}; + callback = callback || function(){}; + + params = { + 'action': 'add-menu-item', + 'menu': menu, + 'menu-settings-column-nonce': nonce, + 'menu-item': menuItem + }; + + $.post( ajaxurl, params, function(menuMarkup) { + var ins = $('#menu-instructions'); + + menuMarkup = menuMarkup || ''; + menuMarkup = menuMarkup.toString().trim(); // Trim leading whitespaces. + processMethod(menuMarkup, params); + + // Make it stand out a bit more visually, by adding a fadeIn. + $( 'li.pending' ).hide().fadeIn('slow'); + $( '.drag-instructions' ).show(); + if( ! ins.hasClass( 'menu-instructions-inactive' ) && ins.siblings().length ) + ins.addClass( 'menu-instructions-inactive' ); + + callback(); + }); + }, + + /** + * Process the add menu item request response into menu list item. Appends to menu. + * + * @param {string} menuMarkup The text server response of menu item markup. + * + * @fires document#menu-item-added Passes menuMarkup as a jQuery object. + */ + addMenuItemToBottom : function( menuMarkup ) { + var $menuMarkup = $( menuMarkup ); + $menuMarkup.hideAdvancedMenuItemFields().appendTo( api.targetList ); + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + wp.a11y.speak( menus.itemAdded ); + $( document ).trigger( 'menu-item-added', [ $menuMarkup ] ); + }, + + /** + * Process the add menu item request response into menu list item. Prepends to menu. + * + * @param {string} menuMarkup The text server response of menu item markup. + * + * @fires document#menu-item-added Passes menuMarkup as a jQuery object. + */ + addMenuItemToTop : function( menuMarkup ) { + var $menuMarkup = $( menuMarkup ); + $menuMarkup.hideAdvancedMenuItemFields().prependTo( api.targetList ); + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + wp.a11y.speak( menus.itemAdded ); + $( document ).trigger( 'menu-item-added', [ $menuMarkup ] ); + }, + + attachUnsavedChangesListener : function() { + $('#menu-management input, #menu-management select, #menu-management, #menu-management textarea, .menu-location-menus select').on( 'change', function(){ + api.registerChange(); + }); + + if ( 0 !== $('#menu-to-edit').length || 0 !== $('.menu-location-menus select').length ) { + window.onbeforeunload = function(){ + if ( api.menusChanged ) + return wp.i18n.__( 'The changes you made will be lost if you navigate away from this page.' ); + }; + } else { + // Make the post boxes read-only, as they can't be used yet. + $( '#menu-settings-column' ).find( 'input,select' ).end().find( 'a' ).attr( 'href', '#' ).off( 'click' ); + } + }, + + registerChange : function() { + api.menusChanged = true; + }, + + attachTabsPanelListeners : function() { + $('#menu-settings-column').on('click', function(e) { + var selectAreaMatch, selectAll, panelId, wrapper, items, + target = $(e.target); + + if ( target.hasClass('nav-tab-link') ) { + + panelId = target.data( 'type' ); + + wrapper = target.parents('.accordion-section-content').first(); + + // Upon changing tabs, we want to uncheck all checkboxes. + $( 'input', wrapper ).prop( 'checked', false ); + + $('.tabs-panel-active', wrapper).removeClass('tabs-panel-active').addClass('tabs-panel-inactive'); + $('#' + panelId, wrapper).removeClass('tabs-panel-inactive').addClass('tabs-panel-active'); + + $('.tabs', wrapper).removeClass('tabs'); + target.parent().addClass('tabs'); + + // Select the search bar. + $('.quick-search', wrapper).trigger( 'focus' ); + + // Hide controls in the search tab if no items found. + if ( ! wrapper.find( '.tabs-panel-active .menu-item-title' ).length ) { + wrapper.addClass( 'has-no-menu-item' ); + } else { + wrapper.removeClass( 'has-no-menu-item' ); + } + + e.preventDefault(); + } else if ( target.hasClass( 'select-all' ) ) { + selectAreaMatch = target.closest( '.button-controls' ).data( 'items-type' ); + if ( selectAreaMatch ) { + items = $( '#' + selectAreaMatch + ' .tabs-panel-active .menu-item-title input' ); + + if ( items.length === items.filter( ':checked' ).length && ! target.is( ':checked' ) ) { + items.prop( 'checked', false ); + } else if ( target.is( ':checked' ) ) { + items.prop( 'checked', true ); + } + } + } else if ( target.hasClass( 'menu-item-checkbox' ) ) { + selectAreaMatch = target.closest( '.tabs-panel-active' ).parent().attr( 'id' ); + if ( selectAreaMatch ) { + items = $( '#' + selectAreaMatch + ' .tabs-panel-active .menu-item-title input' ); + selectAll = $( '.button-controls[data-items-type="' + selectAreaMatch + '"] .select-all' ); + + if ( items.length === items.filter( ':checked' ).length && ! selectAll.is( ':checked' ) ) { + selectAll.prop( 'checked', true ); + } else if ( selectAll.is( ':checked' ) ) { + selectAll.prop( 'checked', false ); + } + } + } else if ( target.hasClass('submit-add-to-menu') ) { + api.registerChange(); + + if ( e.target.id && 'submit-customlinkdiv' == e.target.id ) + api.addCustomLink( api.addMenuItemToBottom ); + else if ( e.target.id && -1 != e.target.id.indexOf('submit-') ) + $('#' + e.target.id.replace(/submit-/, '')).addSelectedToMenu( api.addMenuItemToBottom ); + return false; + } + }); + + /* + * Delegate the `click` event and attach it just to the pagination + * links thus excluding the current page `<span>`. See ticket #35577. + */ + $( '#nav-menu-meta' ).on( 'click', 'a.page-numbers', function() { + var $container = $( this ).closest( '.inside' ); + + $.post( ajaxurl, this.href.replace( /.*\?/, '' ).replace( /action=([^&]*)/, '' ) + '&action=menu-get-metabox', + function( resp ) { + var metaBoxData = JSON.parse( resp ), + toReplace; + + if ( -1 === resp.indexOf( 'replace-id' ) ) { + return; + } + + // Get the post type menu meta box to update. + toReplace = document.getElementById( metaBoxData['replace-id'] ); + + if ( ! metaBoxData.markup || ! toReplace ) { + return; + } + + // Update the post type menu meta box with new content from the response. + $container.html( metaBoxData.markup ); + } + ); + + return false; + }); + }, + + eventOnClickEditLink : function(clickedEl) { + var settings, item, + matchedSection = /#(.*)$/.exec(clickedEl.href); + + if ( matchedSection && matchedSection[1] ) { + settings = $('#'+matchedSection[1]); + item = settings.parent(); + if( 0 !== item.length ) { + if( item.hasClass('menu-item-edit-inactive') ) { + if( ! settings.data('menu-item-data') ) { + settings.data( 'menu-item-data', settings.getItemData() ); + } + settings.slideDown('fast'); + item.removeClass('menu-item-edit-inactive') + .addClass('menu-item-edit-active'); + } else { + settings.slideUp('fast'); + item.removeClass('menu-item-edit-active') + .addClass('menu-item-edit-inactive'); + } + return false; + } + } + }, + + eventOnClickCancelLink : function(clickedEl) { + var settings = $( clickedEl ).closest( '.menu-item-settings' ), + thisMenuItem = $( clickedEl ).closest( '.menu-item' ); + + thisMenuItem.removeClass( 'menu-item-edit-active' ).addClass( 'menu-item-edit-inactive' ); + settings.setItemData( settings.data( 'menu-item-data' ) ).hide(); + // Restore the title of the currently active/expanded menu item. + thisMenuItem.find( '.menu-item-title' ).text( settings.data( 'menu-item-data' )['menu-item-title'] ); + + return false; + }, + + eventOnClickMenuSave : function() { + var locs = '', + menuName = $('#menu-name'), + menuNameVal = menuName.val(); + + // Cancel and warn if invalid menu name. + if ( ! menuNameVal || ! menuNameVal.replace( /\s+/, '' ) ) { + menuName.parent().addClass( 'form-invalid' ); + return false; + } + // Copy menu theme locations. + $('#nav-menu-theme-locations select').each(function() { + locs += '<input type="hidden" name="' + this.name + '" value="' + $(this).val() + '" />'; + }); + $('#update-nav-menu').append( locs ); + // Update menu item position data. + api.menuList.find('.menu-item-data-position').val( function(index) { return index + 1; } ); + window.onbeforeunload = null; + + return true; + }, + + eventOnClickMenuDelete : function() { + // Delete warning AYS. + if ( window.confirm( wp.i18n.__( 'You are about to permanently delete this menu.\n\'Cancel\' to stop, \'OK\' to delete.' ) ) ) { + window.onbeforeunload = null; + return true; + } + return false; + }, + + eventOnClickMenuItemDelete : function(clickedEl) { + var itemID = parseInt(clickedEl.id.replace('delete-', ''), 10); + + api.removeMenuItem( $('#menu-item-' + itemID) ); + api.registerChange(); + return false; + }, + + /** + * Process the quick search response into a search result + * + * @param string resp The server response to the query. + * @param object req The request arguments. + * @param jQuery panel The tabs panel we're searching in. + */ + processQuickSearchQueryResponse : function(resp, req, panel) { + var matched, newID, + takenIDs = {}, + form = document.getElementById('nav-menu-meta'), + pattern = /menu-item[(\[^]\]*/, + $items = $('<div>').html(resp).find('li'), + wrapper = panel.closest( '.accordion-section-content' ), + selectAll = wrapper.find( '.button-controls .select-all' ), + $item; + + if( ! $items.length ) { + $('.categorychecklist', panel).html( '<li><p>' + wp.i18n.__( 'No results found.' ) + '</p></li>' ); + $( '.spinner', panel ).removeClass( 'is-active' ); + wrapper.addClass( 'has-no-menu-item' ); + return; + } + + $items.each(function(){ + $item = $(this); + + // Make a unique DB ID number. + matched = pattern.exec($item.html()); + + if ( matched && matched[1] ) { + newID = matched[1]; + while( form.elements['menu-item[' + newID + '][menu-item-type]'] || takenIDs[ newID ] ) { + newID--; + } + + takenIDs[newID] = true; + if ( newID != matched[1] ) { + $item.html( $item.html().replace(new RegExp( + 'menu-item\\[' + matched[1] + '\\]', 'g'), + 'menu-item[' + newID + ']' + ) ); + } + } + }); + + $('.categorychecklist', panel).html( $items ); + $( '.spinner', panel ).removeClass( 'is-active' ); + wrapper.removeClass( 'has-no-menu-item' ); + + if ( selectAll.is( ':checked' ) ) { + selectAll.prop( 'checked', false ); + } + }, + + /** + * Remove a menu item. + * + * @param {Object} el The element to be removed as a jQuery object. + * + * @fires document#menu-removing-item Passes the element to be removed. + */ + removeMenuItem : function(el) { + var children = el.childMenuItems(); + + $( document ).trigger( 'menu-removing-item', [ el ] ); + el.addClass('deleting').animate({ + opacity : 0, + height: 0 + }, 350, function() { + var ins = $('#menu-instructions'); + el.remove(); + children.shiftDepthClass( -1 ).updateParentMenuItemDBId(); + if ( 0 === $( '#menu-to-edit li' ).length ) { + $( '.drag-instructions' ).hide(); + ins.removeClass( 'menu-instructions-inactive' ); + } + api.refreshAdvancedAccessibility(); + wp.a11y.speak( menus.itemRemoved ); + }); + }, + + depthToPx : function(depth) { + return depth * api.options.menuItemDepthPerLevel; + }, + + pxToDepth : function(px) { + return Math.floor(px / api.options.menuItemDepthPerLevel); + } + + }; + + $( function() { + + wpNavMenu.init(); + + // Prevent focused element from being hidden by the sticky footer. + $( '.menu-edit a, .menu-edit button, .menu-edit input, .menu-edit textarea, .menu-edit select' ).on('focus', function() { + if ( window.innerWidth >= 783 ) { + var navMenuHeight = $( '#nav-menu-footer' ).height() + 20; + var bottomOffset = $(this).offset().top - ( $(window).scrollTop() + $(window).height() - $(this).height() ); + + if ( bottomOffset > 0 ) { + bottomOffset = 0; + } + bottomOffset = bottomOffset * -1; + + if( bottomOffset < navMenuHeight ) { + var scrollTop = $(document).scrollTop(); + $(document).scrollTop( scrollTop + ( navMenuHeight - bottomOffset ) ); + } + } + }); + }); + + // Show bulk action. + $( document ).on( 'menu-item-added', function() { + if ( ! $( '.bulk-actions' ).is( ':visible' ) ) { + $( '.bulk-actions' ).show(); + } + } ); + + // Hide bulk action. + $( document ).on( 'menu-removing-item', function( e, el ) { + var menuElement = $( el ).parents( '#menu-to-edit' ); + if ( menuElement.find( 'li' ).length === 1 && $( '.bulk-actions' ).is( ':visible' ) ) { + $( '.bulk-actions' ).hide(); + } + } ); + +})(jQuery); diff --git a/wp-admin/js/nav-menu.min.js b/wp-admin/js/nav-menu.min.js new file mode 100644 index 0000000..ce28b70 --- /dev/null +++ b/wp-admin/js/nav-menu.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(k){var I=window.wpNavMenu={options:{menuItemDepthPerLevel:30,globalMaxDepth:11,sortableItems:"> *",targetTolerance:0},menuList:void 0,targetList:void 0,menusChanged:!1,isRTL:!("undefined"==typeof isRtl||!isRtl),negateIfRTL:"undefined"!=typeof isRtl&&isRtl?-1:1,lastSearch:"",init:function(){I.menuList=k("#menu-to-edit"),I.targetList=I.menuList,this.jQueryExtensions(),this.attachMenuEditListeners(),this.attachBulkSelectButtonListeners(),this.attachMenuCheckBoxListeners(),this.attachMenuItemDeleteButton(),this.attachPendingMenuItemsListForDeletion(),this.attachQuickSearchListeners(),this.attachThemeLocationsListeners(),this.attachMenuSaveSubmitListeners(),this.attachTabsPanelListeners(),this.attachUnsavedChangesListener(),I.menuList.length&&this.initSortables(),menus.oneThemeLocationNoMenus&&k("#posttype-page").addSelectedToMenu(I.addMenuItemToBottom),this.initManageLocations(),this.initAccessibility(),this.initToggles(),this.initPreviewing()},jQueryExtensions:function(){k.fn.extend({menuItemDepth:function(){var e=I.isRTL?this.eq(0).css("margin-right"):this.eq(0).css("margin-left");return I.pxToDepth(e&&-1!=e.indexOf("px")?e.slice(0,-2):0)},updateDepthClass:function(t,n){return this.each(function(){var e=k(this);n=n||e.menuItemDepth(),k(this).removeClass("menu-item-depth-"+n).addClass("menu-item-depth-"+t)})},shiftDepthClass:function(i){return this.each(function(){var e=k(this),t=e.menuItemDepth(),n=t+i;e.removeClass("menu-item-depth-"+t).addClass("menu-item-depth-"+n),0===n&&e.find(".is-submenu").hide()})},childMenuItems:function(){var i=k();return this.each(function(){for(var e=k(this),t=e.menuItemDepth(),n=e.next(".menu-item");n.length&&n.menuItemDepth()>t;)i=i.add(n),n=n.next(".menu-item")}),i},shiftHorizontally:function(n){return this.each(function(){var e=k(this),t=e.menuItemDepth();e.moveHorizontally(t+n,t)})},moveHorizontally:function(a,s){return this.each(function(){var e=k(this),t=e.childMenuItems(),n=a-s,i=e.find(".is-submenu");e.updateDepthClass(a,s).updateParentMenuItemDBId(),t&&t.each(function(){var e=k(this),t=e.menuItemDepth();e.updateDepthClass(t+n,t).updateParentMenuItemDBId()}),0===a?i.hide():i.show()})},updateParentMenuItemDBId:function(){return this.each(function(){var e=k(this),t=e.find(".menu-item-data-parent-id"),n=parseInt(e.menuItemDepth(),10),e=e.prevAll(".menu-item-depth-"+(n-1)).first();0===n?t.val(0):t.val(e.find(".menu-item-data-db-id").val())})},hideAdvancedMenuItemFields:function(){return this.each(function(){var e=k(this);k(".hide-column-tog").not(":checked").each(function(){e.find(".field-"+k(this).val()).addClass("hidden-field")})})},addSelectedToMenu:function(a){return 0!==k("#menu-to-edit").length&&this.each(function(){var e=k(this),n={},t=menus.oneThemeLocationNoMenus&&0===e.find(".tabs-panel-active .categorychecklist li input:checked").length?e.find('#page-all li input[type="checkbox"]'):e.find(".tabs-panel-active .categorychecklist li input:checked"),i=/menu-item\[([^\]]*)/;if(a=a||I.addMenuItemToBottom,!t.length)return!1;e.find(".button-controls .spinner").addClass("is-active"),k(t).each(function(){var e=k(this),t=i.exec(e.attr("name")),t=void 0===t[1]?0:parseInt(t[1],10);this.className&&-1!=this.className.indexOf("add-to-top")&&(a=I.addMenuItemToTop),n[t]=e.closest("li").getItemData("add-menu-item",t)}),I.addItemToMenu(n,a,function(){t.prop("checked",!1),e.find(".button-controls .select-all").prop("checked",!1),e.find(".button-controls .spinner").removeClass("is-active")})})},getItemData:function(t,n){t=t||"menu-item";var i,a={},s=["menu-item-db-id","menu-item-object-id","menu-item-object","menu-item-parent-id","menu-item-position","menu-item-type","menu-item-title","menu-item-url","menu-item-description","menu-item-attr-title","menu-item-target","menu-item-classes","menu-item-xfn"];return(n=n||"menu-item"!=t?n:this.find(".menu-item-data-db-id").val())&&this.find("input").each(function(){var e;for(i=s.length;i--;)"menu-item"==t?e=s[i]+"["+n+"]":"add-menu-item"==t&&(e="menu-item["+n+"]["+s[i]+"]"),this.name&&e==this.name&&(a[s[i]]=this.value)}),a},setItemData:function(e,a,s){return a=a||"menu-item",(s=s||"menu-item"!=a?s:k(".menu-item-data-db-id",this).val())&&this.find("input").each(function(){var n,i=k(this);k.each(e,function(e,t){"menu-item"==a?n=e+"["+s+"]":"add-menu-item"==a&&(n="menu-item["+s+"]["+e+"]"),n==i.attr("name")&&i.val(t)})}),this}})},countMenuItems:function(e){return k(".menu-item-depth-"+e).length},moveMenuItem:function(e,t){var n,i,a=k("#menu-to-edit li"),s=a.length,m=e.parents("li.menu-item"),o=m.childMenuItems(),u=m.getItemData(),c=parseInt(m.menuItemDepth(),10),l=parseInt(m.index(),10),d=m.next(),r=d.childMenuItems(),h=parseInt(d.menuItemDepth(),10)+1,p=m.prev(),f=parseInt(p.menuItemDepth(),10),v=p.getItemData()["menu-item-db-id"],p=menus["moved"+t.charAt(0).toUpperCase()+t.slice(1)];switch(t){case"up":i=l-1,0!==l&&(0==i&&0!==c&&m.moveHorizontally(0,c),0!==f&&m.moveHorizontally(f,c),(o?n=m.add(o):m).detach().insertBefore(a.eq(i)).updateParentMenuItemDBId());break;case"down":if(o){if(n=m.add(o),(r=0!==(d=a.eq(n.length+l)).childMenuItems().length)&&(i=parseInt(d.menuItemDepth(),10)+1,m.moveHorizontally(i,c)),s===l+n.length)break;n.detach().insertAfter(a.eq(l+n.length)).updateParentMenuItemDBId()}else{if(0!==r.length&&m.moveHorizontally(h,c),s===l+1)break;m.detach().insertAfter(a.eq(l+1)).updateParentMenuItemDBId()}break;case"top":0!==l&&(o?n=m.add(o):m).detach().insertBefore(a.eq(0)).updateParentMenuItemDBId();break;case"left":0!==c&&m.shiftHorizontally(-1);break;case"right":0!==l&&u["menu-item-parent-id"]!==v&&m.shiftHorizontally(1)}e.trigger("focus"),I.registerChange(),I.refreshKeyboardAccessibility(),I.refreshAdvancedAccessibility(),p&&wp.a11y.speak(p)},initAccessibility:function(){var e=k("#menu-to-edit");I.refreshKeyboardAccessibility(),I.refreshAdvancedAccessibility(),e.on("mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility",".menu-item",function(){I.refreshAdvancedAccessibilityOfItem(k(this).find("a.item-edit"))}),e.on("click","a.item-edit",function(){I.refreshAdvancedAccessibilityOfItem(k(this))}),e.on("click",".menus-move",function(){var e=k(this).data("dir");void 0!==e&&I.moveMenuItem(k(this).parents("li.menu-item").find("a.item-edit"),e)})},refreshAdvancedAccessibilityOfItem:function(e){var t,n,i,a,s,m,o,u,c,l,d,r;!0===k(e).data("needs_accessibility_refresh")&&(m=0===(s=(a=(e=k(e)).closest("li.menu-item").first()).menuItemDepth()),o=e.closest(".menu-item-handle").find(".menu-item-title").text(),u=parseInt(a.index(),10),c=m?s:parseInt(s-1,10),c=a.prevAll(".menu-item-depth-"+c).first().find(".menu-item-title").text(),l=a.prevAll(".menu-item-depth-"+s).first().find(".menu-item-title").text(),d=k("#menu-to-edit li").length,r=a.nextAll(".menu-item-depth-"+s).length,a.find(".field-move").toggle(1<d),0!==u&&(i=a.find(".menus-move-up")).attr("aria-label",menus.moveUp).css("display","inline"),0!==u&&m&&(i=a.find(".menus-move-top")).attr("aria-label",menus.moveToTop).css("display","inline"),u+1!==d&&0!==u&&(i=a.find(".menus-move-down")).attr("aria-label",menus.moveDown).css("display","inline"),0===u&&0!==r&&(i=a.find(".menus-move-down")).attr("aria-label",menus.moveDown).css("display","inline"),m||(i=a.find(".menus-move-left"),n=menus.outFrom.replace("%s",c),i.attr("aria-label",menus.moveOutFrom.replace("%s",c)).text(n).css("display","inline")),0!==u&&a.find(".menu-item-data-parent-id").val()!==a.prev().find(".menu-item-data-db-id").val()&&(i=a.find(".menus-move-right"),n=menus.under.replace("%s",l),i.attr("aria-label",menus.moveUnder.replace("%s",l)).text(n).css("display","inline")),n=m?(t=(r=k(".menu-item-depth-0")).index(a)+1,d=r.length,menus.menuFocus.replace("%1$s",o).replace("%2$d",t).replace("%3$d",d)):(u=(c=a.prevAll(".menu-item-depth-"+parseInt(s-1,10)).first()).find(".menu-item-data-db-id").val(),i=c.find(".menu-item-title").text(),l=k('.menu-item .menu-item-data-parent-id[value="'+u+'"]'),t=k(l.parents(".menu-item").get().reverse()).index(a)+1,menus.subMenuFocus.replace("%1$s",o).replace("%2$d",t).replace("%3$s",i)),e.attr("aria-label",n),e.data("needs_accessibility_refresh",!1))},refreshAdvancedAccessibility:function(){k(".menu-item-settings .field-move .menus-move").hide(),k("a.item-edit").data("needs_accessibility_refresh",!0),k(".menu-item-edit-active a.item-edit").each(function(){I.refreshAdvancedAccessibilityOfItem(this)})},refreshKeyboardAccessibility:function(){k("a.item-edit").off("focus").on("focus",function(){k(this).off("keydown").on("keydown",function(e){var t,n=k(this),i=n.parents("li.menu-item").getItemData();if((37==e.which||38==e.which||39==e.which||40==e.which)&&(n.off("keydown"),1!==k("#menu-to-edit li").length)){switch(t={38:"up",40:"down",37:"left",39:"right"},(t=k("body").hasClass("rtl")?{38:"up",40:"down",39:"left",37:"right"}:t)[e.which]){case"up":I.moveMenuItem(n,"up");break;case"down":I.moveMenuItem(n,"down");break;case"left":I.moveMenuItem(n,"left");break;case"right":I.moveMenuItem(n,"right")}return k("#edit-"+i["menu-item-db-id"]).trigger("focus"),!1}})})},initPreviewing:function(){k("#menu-to-edit").on("change input",".edit-menu-item-title",function(e){var e=k(e.currentTarget),t=e.val(),e=e.closest(".menu-item").find(".menu-item-title");t?e.text(t).removeClass("no-title"):e.text(wp.i18n._x("(no label)","missing menu item navigation label")).addClass("no-title")})},initToggles:function(){postboxes.add_postbox_toggles("nav-menus"),columns.useCheckboxesForHidden(),columns.checked=function(e){k(".field-"+e).removeClass("hidden-field")},columns.unchecked=function(e){k(".field-"+e).addClass("hidden-field")},I.menuList.hideAdvancedMenuItemFields(),k(".hide-postbox-tog").on("click",function(){var e=k(".accordion-container li.accordion-section").filter(":hidden").map(function(){return this.id}).get().join(",");k.post(ajaxurl,{action:"closed-postboxes",hidden:e,closedpostboxesnonce:jQuery("#closedpostboxesnonce").val(),page:"nav-menus"})})},initSortables:function(){var m,a,s,n,o,u,c,l,d,r,e,h=0,p=I.menuList.offset().left,f=k("body"),v=f[0].className&&(e=f[0].className.match(/menu-max-depth-(\d+)/))&&e[1]?parseInt(e[1],10):0;function g(e){n=e.placeholder.prev(".menu-item"),o=e.placeholder.next(".menu-item"),n[0]==e.item[0]&&(n=n.prev(".menu-item")),o[0]==e.item[0]&&(o=o.next(".menu-item")),u=n.length?n.offset().top+n.height():0,c=o.length?o.offset().top+o.height()/3:0,a=o.length?o.menuItemDepth():0,s=n.length?(e=n.menuItemDepth()+1)>I.options.globalMaxDepth?I.options.globalMaxDepth:e:0}function b(e,t){e.placeholder.updateDepthClass(t,h),h=t}0!==k("#menu-to-edit li").length&&k(".drag-instructions").show(),p+=I.isRTL?I.menuList.width():0,I.menuList.sortable({handle:".menu-item-handle",placeholder:"sortable-placeholder",items:I.options.sortableItems,start:function(e,t){var n,i;I.isRTL&&(t.item[0].style.right="auto"),d=t.item.children(".menu-item-transport"),m=t.item.menuItemDepth(),b(t,m),i=(t.item.next()[0]==t.placeholder[0]?t.item.next():t.item).childMenuItems(),d.append(i),n=d.outerHeight(),n=(n+=0<n?+t.placeholder.css("margin-top").slice(0,-2):0)+t.helper.outerHeight(),l=n,t.placeholder.height(n-=2),r=m,i.each(function(){var e=k(this).menuItemDepth();r=r<e?e:r}),n=t.helper.find(".menu-item-handle").outerWidth(),n+=I.depthToPx(r-m),t.placeholder.width(n-=2),(i=t.placeholder.next(".menu-item")).css("margin-top",l+"px"),t.placeholder.detach(),k(this).sortable("refresh"),t.item.after(t.placeholder),i.css("margin-top",0),g(t)},stop:function(e,t){var n=h-m,i=d.children().insertAfter(t.item),a=t.item.find(".item-title .is-submenu");if(0<h?a.show():a.hide(),0!=n){t.item.updateDepthClass(h),i.shiftDepthClass(n);var a=n,s=v;if(0!==a){if(0<a)v<(i=r+a)&&(s=i);else if(a<0&&r==v)for(;!k(".menu-item-depth-"+s,I.menuList).length&&0<s;)s--;f.removeClass("menu-max-depth-"+v).addClass("menu-max-depth-"+s),v=s}}I.registerChange(),t.item.updateParentMenuItemDBId(),t.item[0].style.top=0,I.isRTL&&(t.item[0].style.left="auto",t.item[0].style.right=0),I.refreshKeyboardAccessibility(),I.refreshAdvancedAccessibility()},change:function(e,t){t.placeholder.parent().hasClass("menu")||(n.length?n.after(t.placeholder):I.menuList.prepend(t.placeholder)),g(t)},sort:function(e,t){var n=t.helper.offset(),i=I.isRTL?n.left+t.helper.width():n.left,i=I.negateIfRTL*I.pxToDepth(i-p);s<i||n.top<u-I.options.targetTolerance?i=s:i<a&&(i=a),i!=h&&b(t,i),c&&n.top+l>c&&(o.after(t.placeholder),g(t),k(this).sortable("refreshPositions"))}})},initManageLocations:function(){k("#menu-locations-wrap form").on("submit",function(){window.onbeforeunload=null}),k(".menu-location-menus select").on("change",function(){var e=k(this).closest("tr").find(".locations-edit-menu-link");k(this).find("option:selected").data("orig")?e.show():e.hide()})},attachMenuEditListeners:function(){var t=this;k("#update-nav-menu").on("click",function(e){if(e.target&&e.target.className)return-1!=e.target.className.indexOf("item-edit")?t.eventOnClickEditLink(e.target):-1!=e.target.className.indexOf("menu-save")?t.eventOnClickMenuSave(e.target):-1!=e.target.className.indexOf("menu-delete")?t.eventOnClickMenuDelete(e.target):-1!=e.target.className.indexOf("item-delete")?t.eventOnClickMenuItemDelete(e.target):-1!=e.target.className.indexOf("item-cancel")?t.eventOnClickCancelLink(e.target):void 0}),k("#menu-name").on("input",_.debounce(function(){var e=k(document.getElementById("menu-name")),t=e.val();t&&t.replace(/\s+/,"")?e.parent().removeClass("form-invalid"):e.parent().addClass("form-invalid")},500)),k('#add-custom-links input[type="text"]').on("keypress",function(e){k("#customlinkdiv").removeClass("form-invalid"),13===e.keyCode&&(e.preventDefault(),k("#submit-customlinkdiv").trigger("click"))})},attachBulkSelectButtonListeners:function(){var e=this;k(".bulk-select-switcher").on("change",function(){this.checked?(k(".bulk-select-switcher").prop("checked",!0),e.enableBulkSelection()):(k(".bulk-select-switcher").prop("checked",!1),e.disableBulkSelection())})},enableBulkSelection:function(){var e=k("#menu-to-edit .menu-item-checkbox");k("#menu-to-edit").addClass("bulk-selection"),k("#nav-menu-bulk-actions-top").addClass("bulk-selection"),k("#nav-menu-bulk-actions-bottom").addClass("bulk-selection"),k.each(e,function(){k(this).prop("disabled",!1)})},disableBulkSelection:function(){var e=k("#menu-to-edit .menu-item-checkbox");k("#menu-to-edit").removeClass("bulk-selection"),k("#nav-menu-bulk-actions-top").removeClass("bulk-selection"),k("#nav-menu-bulk-actions-bottom").removeClass("bulk-selection"),k(".menu-items-delete").is('[aria-describedby="pending-menu-items-to-delete"]')&&k(".menu-items-delete").removeAttr("aria-describedby"),k.each(e,function(){k(this).prop("disabled",!0).prop("checked",!1)}),k(".menu-items-delete").addClass("disabled"),k("#pending-menu-items-to-delete ul").empty()},attachMenuCheckBoxListeners:function(){var e=this;k("#menu-to-edit").on("change",".menu-item-checkbox",function(){e.setRemoveSelectedButtonStatus()})},attachMenuItemDeleteButton:function(){var t=this;k(document).on("click",".menu-items-delete",function(e){var n,i;e.preventDefault(),k(this).hasClass("disabled")||(k.each(k(".menu-item-checkbox:checked"),function(e,t){k(t).parents("li").find("a.item-delete").trigger("click")}),k(".menu-items-delete").addClass("disabled"),k(".bulk-select-switcher").prop("checked",!1),n="",i=k("#pending-menu-items-to-delete ul li"),k.each(i,function(e,t){t=k(t).find(".pending-menu-item-name").text(),t=menus.menuItemDeletion.replace("%s",t);n+=t,e+1<i.length&&(n+=", ")}),e=menus.itemsDeleted.replace("%s",n),wp.a11y.speak(e,"polite"),t.disableBulkSelection())})},attachPendingMenuItemsListForDeletion:function(){k("#post-body-content").on("change",".menu-item-checkbox",function(){var e,t,n,i;k(".menu-items-delete").is('[aria-describedby="pending-menu-items-to-delete"]')||k(".menu-items-delete").attr("aria-describedby","pending-menu-items-to-delete"),e=k(this).next().text(),t=k(this).parent().next(".item-controls").find(".item-type").text(),n=k(this).attr("data-menu-item-id"),0<(i=k("#pending-menu-items-to-delete ul").find("[data-menu-item-id="+n+"]")).length&&i.remove(),!0===this.checked&&k("#pending-menu-items-to-delete ul").append('<li data-menu-item-id="'+n+'"><span class="pending-menu-item-name">'+e+'</span> <span class="pending-menu-item-type">('+t+')</span><span class="separator"></span></li>'),k("#pending-menu-items-to-delete li .separator").html(", "),k("#pending-menu-items-to-delete li .separator").last().html(".")})},setBulkDeleteCheckboxStatus:function(){var e=k("#menu-to-edit .menu-item-checkbox");k.each(e,function(){k(this).prop("disabled")?k(this).prop("disabled",!1):k(this).prop("disabled",!0),k(this).is(":checked")&&k(this).prop("checked",!1)}),this.setRemoveSelectedButtonStatus()},setRemoveSelectedButtonStatus:function(){var e=k(".menu-items-delete");0<k(".menu-item-checkbox:checked").length?e.removeClass("disabled"):e.addClass("disabled")},attachMenuSaveSubmitListeners:function(){k("#update-nav-menu").on("submit",function(){var e=k("#update-nav-menu").serializeArray();k('[name="nav-menu-data"]').val(JSON.stringify(e))})},attachThemeLocationsListeners:function(){var e=k("#nav-menu-theme-locations"),t={action:"menu-locations-save"};t["menu-settings-column-nonce"]=k("#menu-settings-column-nonce").val(),e.find('input[type="submit"]').on("click",function(){return e.find("select").each(function(){t[this.name]=k(this).val()}),e.find(".spinner").addClass("is-active"),k.post(ajaxurl,t,function(){e.find(".spinner").removeClass("is-active")}),!1})},attachQuickSearchListeners:function(){var t;k("#nav-menu-meta").on("submit",function(e){e.preventDefault()}),k("#nav-menu-meta").on("input",".quick-search",function(){var e=k(this);e.attr("autocomplete","off"),t&&clearTimeout(t),t=setTimeout(function(){I.updateQuickSearchResults(e)},500)}).on("blur",".quick-search",function(){I.lastSearch=""})},updateQuickSearchResults:function(e){var t,n,i=e.val();i.length<2||I.lastSearch==i||(I.lastSearch=i,t=e.parents(".tabs-panel"),n={action:"menu-quick-search","response-format":"markup",menu:k("#menu").val(),"menu-settings-column-nonce":k("#menu-settings-column-nonce").val(),q:i,type:e.attr("name")},k(".spinner",t).addClass("is-active"),k.post(ajaxurl,n,function(e){I.processQuickSearchQueryResponse(e,n,t)}))},addCustomLink:function(e){var t=k("#custom-menu-item-url").val().toString(),n=k("#custom-menu-item-name").val();if(""!==t&&(t=t.trim()),e=e||I.addMenuItemToBottom,""===t||"https://"==t||"http://"==t)return k("#customlinkdiv").addClass("form-invalid"),!1;k(".customlinkdiv .spinner").addClass("is-active"),this.addLinkToMenu(t,n,e,function(){k(".customlinkdiv .spinner").removeClass("is-active"),k("#custom-menu-item-name").val("").trigger("blur"),k("#custom-menu-item-url").val("").attr("placeholder","https://")})},addLinkToMenu:function(e,t,n,i){n=n||I.addMenuItemToBottom,I.addItemToMenu({"-1":{"menu-item-type":"custom","menu-item-url":e,"menu-item-title":t}},n,i=i||function(){})},addItemToMenu:function(e,n,i){var a,t=k("#menu").val(),s=k("#menu-settings-column-nonce").val();n=n||function(){},i=i||function(){},a={action:"add-menu-item",menu:t,"menu-settings-column-nonce":s,"menu-item":e},k.post(ajaxurl,a,function(e){var t=k("#menu-instructions");e=(e=e||"").toString().trim(),n(e,a),k("li.pending").hide().fadeIn("slow"),k(".drag-instructions").show(),!t.hasClass("menu-instructions-inactive")&&t.siblings().length&&t.addClass("menu-instructions-inactive"),i()})},addMenuItemToBottom:function(e){e=k(e);e.hideAdvancedMenuItemFields().appendTo(I.targetList),I.refreshKeyboardAccessibility(),I.refreshAdvancedAccessibility(),wp.a11y.speak(menus.itemAdded),k(document).trigger("menu-item-added",[e])},addMenuItemToTop:function(e){e=k(e);e.hideAdvancedMenuItemFields().prependTo(I.targetList),I.refreshKeyboardAccessibility(),I.refreshAdvancedAccessibility(),wp.a11y.speak(menus.itemAdded),k(document).trigger("menu-item-added",[e])},attachUnsavedChangesListener:function(){k("#menu-management input, #menu-management select, #menu-management, #menu-management textarea, .menu-location-menus select").on("change",function(){I.registerChange()}),0!==k("#menu-to-edit").length||0!==k(".menu-location-menus select").length?window.onbeforeunload=function(){if(I.menusChanged)return wp.i18n.__("The changes you made will be lost if you navigate away from this page.")}:k("#menu-settings-column").find("input,select").end().find("a").attr("href","#").off("click")},registerChange:function(){I.menusChanged=!0},attachTabsPanelListeners:function(){k("#menu-settings-column").on("click",function(e){var t,n,i,a,s=k(e.target);if(s.hasClass("nav-tab-link"))n=s.data("type"),i=s.parents(".accordion-section-content").first(),k("input",i).prop("checked",!1),k(".tabs-panel-active",i).removeClass("tabs-panel-active").addClass("tabs-panel-inactive"),k("#"+n,i).removeClass("tabs-panel-inactive").addClass("tabs-panel-active"),k(".tabs",i).removeClass("tabs"),s.parent().addClass("tabs"),k(".quick-search",i).trigger("focus"),i.find(".tabs-panel-active .menu-item-title").length?i.removeClass("has-no-menu-item"):i.addClass("has-no-menu-item"),e.preventDefault();else if(s.hasClass("select-all"))(t=s.closest(".button-controls").data("items-type"))&&((a=k("#"+t+" .tabs-panel-active .menu-item-title input")).length!==a.filter(":checked").length||s.is(":checked")?s.is(":checked")&&a.prop("checked",!0):a.prop("checked",!1));else if(s.hasClass("menu-item-checkbox"))(t=s.closest(".tabs-panel-active").parent().attr("id"))&&(a=k("#"+t+" .tabs-panel-active .menu-item-title input"),n=k('.button-controls[data-items-type="'+t+'"] .select-all'),a.length!==a.filter(":checked").length||n.is(":checked")?n.is(":checked")&&n.prop("checked",!1):n.prop("checked",!0));else if(s.hasClass("submit-add-to-menu"))return I.registerChange(),e.target.id&&"submit-customlinkdiv"==e.target.id?I.addCustomLink(I.addMenuItemToBottom):e.target.id&&-1!=e.target.id.indexOf("submit-")&&k("#"+e.target.id.replace(/submit-/,"")).addSelectedToMenu(I.addMenuItemToBottom),!1}),k("#nav-menu-meta").on("click","a.page-numbers",function(){var n=k(this).closest(".inside");return k.post(ajaxurl,this.href.replace(/.*\?/,"").replace(/action=([^&]*)/,"")+"&action=menu-get-metabox",function(e){var t=JSON.parse(e);-1!==e.indexOf("replace-id")&&(e=document.getElementById(t["replace-id"]),t.markup)&&e&&n.html(t.markup)}),!1})},eventOnClickEditLink:function(e){var t,e=/#(.*)$/.exec(e.href);if(e&&e[1]&&0!==(t=(e=k("#"+e[1])).parent()).length)return t.hasClass("menu-item-edit-inactive")?(e.data("menu-item-data")||e.data("menu-item-data",e.getItemData()),e.slideDown("fast"),t.removeClass("menu-item-edit-inactive").addClass("menu-item-edit-active")):(e.slideUp("fast"),t.removeClass("menu-item-edit-active").addClass("menu-item-edit-inactive")),!1},eventOnClickCancelLink:function(e){var t=k(e).closest(".menu-item-settings"),e=k(e).closest(".menu-item");return e.removeClass("menu-item-edit-active").addClass("menu-item-edit-inactive"),t.setItemData(t.data("menu-item-data")).hide(),e.find(".menu-item-title").text(t.data("menu-item-data")["menu-item-title"]),!1},eventOnClickMenuSave:function(){var e="",t=k("#menu-name"),n=t.val();return n&&n.replace(/\s+/,"")?(k("#nav-menu-theme-locations select").each(function(){e+='<input type="hidden" name="'+this.name+'" value="'+k(this).val()+'" />'}),k("#update-nav-menu").append(e),I.menuList.find(".menu-item-data-position").val(function(e){return e+1}),!(window.onbeforeunload=null)):(t.parent().addClass("form-invalid"),!1)},eventOnClickMenuDelete:function(){return!!window.confirm(wp.i18n.__("You are about to permanently delete this menu.\n'Cancel' to stop, 'OK' to delete."))&&!(window.onbeforeunload=null)},eventOnClickMenuItemDelete:function(e){e=parseInt(e.id.replace("delete-",""),10);return I.removeMenuItem(k("#menu-item-"+e)),I.registerChange(),!1},processQuickSearchQueryResponse:function(e,t,n){var i,a,s,m={},o=document.getElementById("nav-menu-meta"),u=/menu-item[(\[^]\]*/,e=k("<div>").html(e).find("li"),c=n.closest(".accordion-section-content"),l=c.find(".button-controls .select-all");e.length?(e.each(function(){if(s=k(this),(i=u.exec(s.html()))&&i[1]){for(a=i[1];o.elements["menu-item["+a+"][menu-item-type]"]||m[a];)a--;m[a]=!0,a!=i[1]&&s.html(s.html().replace(new RegExp("menu-item\\["+i[1]+"\\]","g"),"menu-item["+a+"]"))}}),k(".categorychecklist",n).html(e),k(".spinner",n).removeClass("is-active"),c.removeClass("has-no-menu-item"),l.is(":checked")&&l.prop("checked",!1)):(k(".categorychecklist",n).html("<li><p>"+wp.i18n.__("No results found.")+"</p></li>"),k(".spinner",n).removeClass("is-active"),c.addClass("has-no-menu-item"))},removeMenuItem:function(t){var n=t.childMenuItems();k(document).trigger("menu-removing-item",[t]),t.addClass("deleting").animate({opacity:0,height:0},350,function(){var e=k("#menu-instructions");t.remove(),n.shiftDepthClass(-1).updateParentMenuItemDBId(),0===k("#menu-to-edit li").length&&(k(".drag-instructions").hide(),e.removeClass("menu-instructions-inactive")),I.refreshAdvancedAccessibility(),wp.a11y.speak(menus.itemRemoved)})},depthToPx:function(e){return e*I.options.menuItemDepthPerLevel},pxToDepth:function(e){return Math.floor(e/I.options.menuItemDepthPerLevel)}};k(function(){wpNavMenu.init(),k(".menu-edit a, .menu-edit button, .menu-edit input, .menu-edit textarea, .menu-edit select").on("focus",function(){var e,t,n;783<=window.innerWidth&&(e=k("#nav-menu-footer").height()+20,0<(t=k(this).offset().top-(k(window).scrollTop()+k(window).height()-k(this).height()))&&(t=0),(t*=-1)<e)&&(n=k(document).scrollTop(),k(document).scrollTop(n+(e-t)))})}),k(document).on("menu-item-added",function(){k(".bulk-actions").is(":visible")||k(".bulk-actions").show()}),k(document).on("menu-removing-item",function(e,t){1===k(t).parents("#menu-to-edit").find("li").length&&k(".bulk-actions").is(":visible")&&k(".bulk-actions").hide()})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/password-strength-meter.js b/wp-admin/js/password-strength-meter.js new file mode 100644 index 0000000..506088b --- /dev/null +++ b/wp-admin/js/password-strength-meter.js @@ -0,0 +1,149 @@ +/** + * @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); diff --git a/wp-admin/js/password-strength-meter.min.js b/wp-admin/js/password-strength-meter.min.js new file mode 100644 index 0000000..3e0bef0 --- /dev/null +++ b/wp-admin/js/password-strength-meter.min.js @@ -0,0 +1,2 @@ +/*! 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<t.length?5:void 0===window.zxcvbn?-1:zxcvbn(e,n).score},userInputBlacklist:function(){return window.console.log(n(e("%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()")),wp.passwordStrength.userInputDisallowedList()},userInputDisallowedList:function(){var e,n,t,r,s=[],i=[],o=["user_login","first_name","last_name","nickname","display_name","email","url","description","weblog_title","admin_email"];for(s.push(document.title),s.push(document.URL),n=o.length,e=0;e<n;e++)0!==(r=a("#"+o[e])).length&&(s.push(r[0].defaultValue),s.push(r.val()));for(t=s.length,e=0;e<t;e++)s[e]&&(i=i.concat(s[e].replace(/\W/g," ").split(" ")));return i=a.grep(i,function(e,n){return!(""===e||e.length<4)&&a.inArray(e,i)===n})}},window.passwordStrength=wp.passwordStrength.meter}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/password-toggle.js b/wp-admin/js/password-toggle.js new file mode 100644 index 0000000..5bfaa3b --- /dev/null +++ b/wp-admin/js/password-toggle.js @@ -0,0 +1,40 @@ +/** + * 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' ); + } + } +} )(); diff --git a/wp-admin/js/password-toggle.min.js b/wp-admin/js/password-toggle.min.js new file mode 100644 index 0000000..c58ef9f --- /dev/null +++ b/wp-admin/js/password-toggle.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(){var t,e,s,i,a=wp.i18n.__;function d(){t=this.getAttribute("data-toggle"),e=this.parentElement.children.namedItem("pwd"),s=this.getElementsByClassName("dashicons")[0],i=this.getElementsByClassName("text")[0],0===parseInt(t,10)?(this.setAttribute("data-toggle",1),this.setAttribute("aria-label",a("Hide password")),e.setAttribute("type","text"),i.innerHTML=a("Hide"),s.classList.remove("dashicons-visibility"),s.classList.add("dashicons-hidden")):(this.setAttribute("data-toggle",0),this.setAttribute("aria-label",a("Show password")),e.setAttribute("type","password"),i.innerHTML=a("Show"),s.classList.remove("dashicons-hidden"),s.classList.add("dashicons-visibility"))}document.querySelectorAll(".pwd-toggle").forEach(function(t){t.classList.remove("hide-if-no-js"),t.addEventListener("click",d)})}();
\ No newline at end of file diff --git a/wp-admin/js/plugin-install.js b/wp-admin/js/plugin-install.js new file mode 100644 index 0000000..9b43b53 --- /dev/null +++ b/wp-admin/js/plugin-install.js @@ -0,0 +1,229 @@ +/** + * @file Functionality for the plugin install screens. + * + * @output wp-admin/js/plugin-install.js + */ + +/* global tb_click, tb_remove, tb_position */ + +jQuery( function( $ ) { + + var tbWindow, + $iframeBody, + $tabbables, + $firstTabbable, + $lastTabbable, + $focusedBefore = $(), + $uploadViewToggle = $( '.upload-view-toggle' ), + $wrap = $ ( '.wrap' ), + $body = $( document.body ); + + window.tb_position = function() { + var width = $( window ).width(), + H = $( window ).height() - ( ( 792 < width ) ? 60 : 20 ), + W = ( 792 < width ) ? 772 : width - 20; + + tbWindow = $( '#TB_window' ); + + if ( tbWindow.length ) { + tbWindow.width( W ).height( H ); + $( '#TB_iframeContent' ).width( W ).height( H ); + tbWindow.css({ + 'margin-left': '-' + parseInt( ( W / 2 ), 10 ) + 'px' + }); + if ( typeof document.body.style.maxWidth !== 'undefined' ) { + tbWindow.css({ + 'top': '30px', + 'margin-top': '0' + }); + } + } + + return $( 'a.thickbox' ).each( function() { + var href = $( this ).attr( 'href' ); + if ( ! href ) { + return; + } + href = href.replace( /&width=[0-9]+/g, '' ); + href = href.replace( /&height=[0-9]+/g, '' ); + $(this).attr( 'href', href + '&width=' + W + '&height=' + ( H ) ); + }); + }; + + $( window ).on( 'resize', function() { + tb_position(); + }); + + /* + * Custom events: when a Thickbox iframe has loaded and when the Thickbox + * modal gets removed from the DOM. + */ + $body + .on( 'thickbox:iframe:loaded', tbWindow, function() { + /* + * Return if it's not the modal with the plugin details iframe. Other + * thickbox instances might want to load an iframe with content from + * an external domain. Avoid to access the iframe contents when we're + * not sure the iframe loads from the same domain. + */ + if ( ! tbWindow.hasClass( 'plugin-details-modal' ) ) { + return; + } + + iframeLoaded(); + }) + .on( 'thickbox:removed', function() { + // Set focus back to the element that opened the modal dialog. + // Note: IE 8 would need this wrapped in a fake setTimeout `0`. + $focusedBefore.trigger( 'focus' ); + }); + + function iframeLoaded() { + var $iframe = tbWindow.find( '#TB_iframeContent' ); + + // Get the iframe body. + $iframeBody = $iframe.contents().find( 'body' ); + + // Get the tabbable elements and handle the keydown event on first load. + handleTabbables(); + + // Set initial focus on the "Close" button. + $firstTabbable.trigger( 'focus' ); + + /* + * When the "Install" button is disabled (e.g. the Plugin is already installed) + * then we can't predict where the last focusable element is. We need to get + * the tabbable elements and handle the keydown event again and again, + * each time the active tab panel changes. + */ + $( '#plugin-information-tabs a', $iframeBody ).on( 'click', function() { + handleTabbables(); + }); + + // Close the modal when pressing Escape. + $iframeBody.on( 'keydown', function( event ) { + if ( 27 !== event.which ) { + return; + } + tb_remove(); + }); + } + + /* + * Get the tabbable elements and detach/attach the keydown event. + * Called after the iframe has fully loaded so we have all the elements we need. + * Called again each time a Tab gets clicked. + * @todo Consider to implement a WordPress general utility for this and don't use jQuery UI. + */ + function handleTabbables() { + var $firstAndLast; + // Get all the tabbable elements. + $tabbables = $( ':tabbable', $iframeBody ); + // Our first tabbable element is always the "Close" button. + $firstTabbable = tbWindow.find( '#TB_closeWindowButton' ); + // Get the last tabbable element. + $lastTabbable = $tabbables.last(); + // Make a jQuery collection. + $firstAndLast = $firstTabbable.add( $lastTabbable ); + // Detach any previously attached keydown event. + $firstAndLast.off( 'keydown.wp-plugin-details' ); + // Attach again the keydown event on the first and last focusable elements. + $firstAndLast.on( 'keydown.wp-plugin-details', function( event ) { + constrainTabbing( event ); + }); + } + + // Constrain tabbing within the plugin modal dialog. + function constrainTabbing( event ) { + if ( 9 !== event.which ) { + return; + } + + if ( $lastTabbable[0] === event.target && ! event.shiftKey ) { + event.preventDefault(); + $firstTabbable.trigger( 'focus' ); + } else if ( $firstTabbable[0] === event.target && event.shiftKey ) { + event.preventDefault(); + $lastTabbable.trigger( 'focus' ); + } + } + + /* + * Open the Plugin details modal. The event is delegated to get also the links + * in the plugins search tab, after the Ajax search rebuilds the HTML. It's + * delegated on the closest ancestor and not on the body to avoid conflicts + * with other handlers, see Trac ticket #43082. + */ + $( '.wrap' ).on( 'click', '.thickbox.open-plugin-details-modal', function( e ) { + // The `data-title` attribute is used only in the Plugin screens. + var title = $( this ).data( 'title' ) ? + wp.i18n.sprintf( + // translators: %s: Plugin name. + wp.i18n.__( 'Plugin: %s' ), + $( this ).data( 'title' ) + ) : + wp.i18n.__( 'Plugin details' ); + + e.preventDefault(); + e.stopPropagation(); + + // Store the element that has focus before opening the modal dialog, i.e. the control which opens it. + $focusedBefore = $( this ); + + tb_click.call(this); + + // Set ARIA role, ARIA label, and add a CSS class. + tbWindow + .attr({ + 'role': 'dialog', + 'aria-label': wp.i18n.__( 'Plugin details' ) + }) + .addClass( 'plugin-details-modal' ); + + // Set title attribute on the iframe. + tbWindow.find( '#TB_iframeContent' ).attr( 'title', title ); + }); + + /* Plugin install related JS */ + $( '#plugin-information-tabs a' ).on( 'click', function( event ) { + var tab = $( this ).attr( 'name' ); + event.preventDefault(); + + // Flip the tab. + $( '#plugin-information-tabs a.current' ).removeClass( 'current' ); + $( this ).addClass( 'current' ); + + // Only show the fyi box in the description section, on smaller screen, + // where it's otherwise always displayed at the top. + if ( 'description' !== tab && $( window ).width() < 772 ) { + $( '#plugin-information-content' ).find( '.fyi' ).hide(); + } else { + $( '#plugin-information-content' ).find( '.fyi' ).show(); + } + + // Flip the content. + $( '#section-holder div.section' ).hide(); // Hide 'em all. + $( '#section-' + tab ).show(); + }); + + /* + * When a user presses the "Upload Plugin" button, show the upload form in place + * rather than sending them to the devoted upload plugin page. + * The `?tab=upload` page still exists for no-js support and for plugins that + * might access it directly. When we're in this page, let the link behave + * like a link. Otherwise we're in the normal plugin installer pages and the + * link should behave like a toggle button. + */ + if ( ! $wrap.hasClass( 'plugin-install-tab-upload' ) ) { + $uploadViewToggle + .attr({ + role: 'button', + 'aria-expanded': 'false' + }) + .on( 'click', function( event ) { + event.preventDefault(); + $body.toggleClass( 'show-upload-view' ); + $uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) ); + }); + } +}); diff --git a/wp-admin/js/plugin-install.min.js b/wp-admin/js/plugin-install.min.js new file mode 100644 index 0000000..172e679 --- /dev/null +++ b/wp-admin/js/plugin-install.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(e){var o,i,n,a,l,r=e(),s=e(".upload-view-toggle"),t=e(".wrap"),d=e(document.body);function c(){var t;n=e(":tabbable",i),a=o.find("#TB_closeWindowButton"),l=n.last(),(t=a.add(l)).off("keydown.wp-plugin-details"),t.on("keydown.wp-plugin-details",function(t){9===(t=t).which&&(l[0]!==t.target||t.shiftKey?a[0]===t.target&&t.shiftKey&&(t.preventDefault(),l.trigger("focus")):(t.preventDefault(),a.trigger("focus")))})}window.tb_position=function(){var t=e(window).width(),i=e(window).height()-(792<t?60:20),n=792<t?772:t-20;return(o=e("#TB_window")).length&&(o.width(n).height(i),e("#TB_iframeContent").width(n).height(i),o.css({"margin-left":"-"+parseInt(n/2,10)+"px"}),void 0!==document.body.style.maxWidth)&&o.css({top:"30px","margin-top":"0"}),e("a.thickbox").each(function(){var t=e(this).attr("href");t&&(t=(t=t.replace(/&width=[0-9]+/g,"")).replace(/&height=[0-9]+/g,""),e(this).attr("href",t+"&width="+n+"&height="+i))})},e(window).on("resize",function(){tb_position()}),d.on("thickbox:iframe:loaded",o,function(){var t;o.hasClass("plugin-details-modal")&&(t=o.find("#TB_iframeContent"),i=t.contents().find("body"),c(),a.trigger("focus"),e("#plugin-information-tabs a",i).on("click",function(){c()}),i.on("keydown",function(t){27===t.which&&tb_remove()}))}).on("thickbox:removed",function(){r.trigger("focus")}),e(".wrap").on("click",".thickbox.open-plugin-details-modal",function(t){var i=e(this).data("title")?wp.i18n.sprintf(wp.i18n.__("Plugin: %s"),e(this).data("title")):wp.i18n.__("Plugin details");t.preventDefault(),t.stopPropagation(),r=e(this),tb_click.call(this),o.attr({role:"dialog","aria-label":wp.i18n.__("Plugin details")}).addClass("plugin-details-modal"),o.find("#TB_iframeContent").attr("title",i)}),e("#plugin-information-tabs a").on("click",function(t){var i=e(this).attr("name");t.preventDefault(),e("#plugin-information-tabs a.current").removeClass("current"),e(this).addClass("current"),"description"!==i&&e(window).width()<772?e("#plugin-information-content").find(".fyi").hide():e("#plugin-information-content").find(".fyi").show(),e("#section-holder div.section").hide(),e("#section-"+i).show()}),t.hasClass("plugin-install-tab-upload")||s.attr({role:"button","aria-expanded":"false"}).on("click",function(t){t.preventDefault(),d.toggleClass("show-upload-view"),s.attr("aria-expanded",d.hasClass("show-upload-view"))})});
\ No newline at end of file diff --git a/wp-admin/js/post.js b/wp-admin/js/post.js new file mode 100644 index 0000000..a86ea4c --- /dev/null +++ b/wp-admin/js/post.js @@ -0,0 +1,1375 @@ +/** + * @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('<tr><td colspan="2">'+wpAjax.broken+'</td></tr>'); + } + ); + + 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 = $( '<img />', { + '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 15 seconds if post lock dialogs are enabled. + if ( wp.heartbeat && $('#post-lock-dialog').length ) { + wp.heartbeat.interval( 15 ); + } + + // 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(''); + }); + + // This code is meant to allow tabbing from Title to Post content. + $('#title').on( 'keydown.editor-focus', function( event ) { + var editor; + + if ( event.keyCode === 9 && ! event.ctrlKey && ! event.altKey && ! event.shiftKey ) { + editor = typeof tinymce != 'undefined' && tinymce.get('content'); + + if ( editor && ! editor.isHidden() ) { + editor.focus(); + } else if ( $textarea.length ) { + $textarea.trigger( 'focus' ); + } else { + return; + } + + event.preventDefault(); + } + }); + + // 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 ) + $('#in-' + taxonomy + '-' + id + ', #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 + ' <b>' + + // 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 ) ) + + '</b> ' + ); + } + + // 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('<option value="publish">' + __( 'Privately Published' ) + '</option>'); + } 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 - 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 - 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( + '<button type="button" class="save button button-small">' + __( 'OK' ) + '</button> ' + + '<button type="button" class="cancel button-link">' + __( 'Cancel' ) + '</button>' + ); + + // 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( + '<label for="new-post-slug" class="screen-reader-text">' + slug_label + '</label>' + + '<input type="text" id="new-post-slug" value="' + slug_value + '" autocomplete="off" spellcheck="false" />' + ).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 sanity 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' ); + + // Sanity 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(); + // Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680 + triggerElement.trigger( 'focus' ); + + // 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() ); diff --git a/wp-admin/js/post.min.js b/wp-admin/js/post.min.js new file mode 100644 index 0000000..1bbdd27 --- /dev/null +++ b/wp-admin/js/post.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.makeSlugeditClickable=window.editPermalink=function(){},window.wp=window.wp||{},function(s){var t=!1,a=wp.i18n.__;window.commentsBox={st:0,get:function(t,e){var i=this.st;return this.st+=e=e||20,this.total=t,s("#commentsdiv .spinner").addClass("is-active"),t={action:"get-comments",mode:"single",_ajax_nonce:s("#add_comment_nonce").val(),p:s("#post_ID").val(),start:i,number:e},s.post(ajaxurl,t,function(t){t=wpAjax.parseAjaxResponse(t),s("#commentsdiv .widefat").show(),s("#commentsdiv .spinner").removeClass("is-active"),"object"==typeof t&&t.responses[0]?(s("#the-comment-list").append(t.responses[0].data),theList=theExtraList=null,s("a[className*=':']").off(),commentsBox.st>commentsBox.total?s("#show-comments").hide():s("#show-comments").show().children("a").text(a("Show more comments"))):1==t?s("#show-comments").text(a("No more comments found.")):s("#the-comment-list").append('<tr><td colspan="2">'+wpAjax.broken+"</td></tr>")}),!1},load:function(t){this.st=jQuery("#the-comment-list tr.comment:visible").length,this.get(t)}},window.WPSetThumbnailHTML=function(t){s(".inside","#postimagediv").html(t)},window.WPSetThumbnailID=function(t){var e=s('input[value="_thumbnail_id"]',"#list-table");0<e.length&&s("#meta\\["+e.attr("id").match(/[0-9]+/)+"\\]\\[value\\]").text(t)},window.WPRemoveThumbnail=function(t){s.post(ajaxurl,{action:"set-post-thumbnail",post_id:s("#post_ID").val(),thumbnail_id:-1,_ajax_nonce:t,cookie:encodeURIComponent(document.cookie)},function(t){"0"==t?alert(a("Could not set that as the thumbnail image. Try a different attachment.")):WPSetThumbnailHTML(t)})},s(document).on("heartbeat-send.refresh-lock",function(t,e){var i=s("#active_post_lock").val(),a=s("#post_ID").val(),n={};a&&s("#post-lock-dialog").length&&(n.post_id=a,i&&(n.lock=i),e["wp-refresh-post-lock"]=n)}).on("heartbeat-tick.refresh-lock",function(t,e){var i,a;e["wp-refresh-post-lock"]&&((e=e["wp-refresh-post-lock"]).lock_error?(i=s("#post-lock-dialog")).length&&!i.is(":visible")&&(wp.autosave&&(s(document).one("heartbeat-tick",function(){wp.autosave.server.suspend(),i.removeClass("saving").addClass("saved"),s(window).off("beforeunload.edit-post")}),i.addClass("saving"),wp.autosave.server.triggerSave()),e.lock_error.avatar_src&&(a=s("<img />",{class:"avatar avatar-64 photo",width:64,height:64,alt:"",src:e.lock_error.avatar_src,srcset:e.lock_error.avatar_src_2x?e.lock_error.avatar_src_2x+" 2x":void 0}),i.find("div.post-locked-avatar").empty().append(a)),i.show().find(".currently-editing").text(e.lock_error.text),i.find(".wp-tab-first").trigger("focus")):e.new_lock&&s("#active_post_lock").val(e.new_lock))}).on("before-autosave.update-post-slug",function(){t=document.activeElement&&"title"===document.activeElement.id}).on("after-autosave.update-post-slug",function(){s("#edit-slug-box > *").length||t||s.post(ajaxurl,{action:"sample-permalink",post_id:s("#post_ID").val(),new_title:s("#title").val(),samplepermalinknonce:s("#samplepermalinknonce").val()},function(t){"-1"!=t&&s("#edit-slug-box").html(t)})})}(jQuery),function(a){var n,t;function i(){n=!1,window.clearTimeout(t),t=window.setTimeout(function(){n=!0},3e5)}a(function(){i()}).on("heartbeat-send.wp-refresh-nonces",function(t,e){var i=a("#wp-auth-check-wrap");(n||i.length&&!i.hasClass("hidden"))&&(i=a("#post_ID").val())&&a("#_wpnonce").val()&&(e["wp-refresh-post-nonces"]={post_id:i})}).on("heartbeat-tick.wp-refresh-nonces",function(t,e){e=e["wp-refresh-post-nonces"];e&&(i(),e.replace&&a.each(e.replace,function(t,e){a("#"+t).val(e)}),e.heartbeatNonce)&&(window.heartbeatSettings.nonce=e.heartbeatNonce)})}(jQuery),jQuery(function(h){var p,e,i,a,n,s,o,l,r,t,c,d,u=h("#content"),f=h(document),v=h("#post_ID").val()||0,m=h("#submitpost"),g=!0,w=h("#post-visibility-select"),b=h("#timestampdiv"),k=h("#post-status-select"),_=!!window.navigator.platform&&-1!==window.navigator.platform.indexOf("Mac"),y=new ClipboardJS(".copy-attachment-url.edit-media"),x=wp.i18n.__,C=wp.i18n._x;function D(t){c.hasClass("wp-editor-expand")||(r?o.theme.resizeTo(null,l+t.pageY):u.height(Math.max(50,l+t.pageY)),t.preventDefault())}function j(){var t;c.hasClass("wp-editor-expand")||(t=r?(o.focus(),((t=parseInt(h("#wp-content-editor-container .mce-toolbar-grp").height(),10))<10||200<t)&&(t=30),parseInt(h("#content_ifr").css("height"),10)+t-28):(u.trigger("focus"),parseInt(u.css("height"),10)),f.off(".wp-editor-resize"),t&&50<t&&t<5e3&&setUserSetting("ed_size",t))}postboxes.add_postbox_toggles(pagenow),window.name="",h("#post-lock-dialog .notification-dialog").on("keydown",function(t){var e;9==t.which&&((e=h(t.target)).hasClass("wp-tab-first")&&t.shiftKey?(h(this).find(".wp-tab-last").trigger("focus"),t.preventDefault()):e.hasClass("wp-tab-last")&&!t.shiftKey&&(h(this).find(".wp-tab-first").trigger("focus"),t.preventDefault()))}).filter(":visible").find(".wp-tab-first").trigger("focus"),wp.heartbeat&&h("#post-lock-dialog").length&&wp.heartbeat.interval(15),i=m.find(":submit, a.submitdelete, #post-preview").on("click.edit-post",function(t){var e=h(this);e.hasClass("disabled")?t.preventDefault():e.hasClass("submitdelete")||e.is("#post-preview")||h("form#post").off("submit.edit-post").on("submit.edit-post",function(t){if(!t.isDefaultPrevented()){if(wp.autosave&&wp.autosave.server.suspend(),"undefined"!=typeof commentReply){if(!commentReply.discardCommentChanges())return!1;commentReply.close()}g=!1,h(window).off("beforeunload.edit-post"),i.addClass("disabled"),("publish"===e.attr("id")?m.find("#major-publishing-actions .spinner"):m.find("#minor-publishing .spinner")).addClass("is-active")}})}),h("#post-preview").on("click.post-preview",function(t){var e=h(this),i=h("form#post"),a=h("input#wp-preview"),n=e.attr("target")||"wp-preview",s=navigator.userAgent.toLowerCase();t.preventDefault(),e.hasClass("disabled")||(wp.autosave&&wp.autosave.server.tempBlockSave(),a.val("dopreview"),i.attr("target",n).trigger("submit").attr("target",""),-1!==s.indexOf("safari")&&-1===s.indexOf("chrome")&&i.attr("action",function(t,e){return e+"?t="+(new Date).getTime()}),a.val(""))}),h("#title").on("keydown.editor-focus",function(t){var e;if(9===t.keyCode&&!t.ctrlKey&&!t.altKey&&!t.shiftKey){if((e="undefined"!=typeof tinymce&&tinymce.get("content"))&&!e.isHidden())e.focus();else{if(!u.length)return;u.trigger("focus")}t.preventDefault()}}),h("#auto_draft").val()&&h("#title").on("blur",function(){var t;this.value&&!h("#edit-slug-box > *").length&&(h("form#post").one("submit",function(){t=!0}),window.setTimeout(function(){!t&&wp.autosave&&wp.autosave.server.triggerSave()},200))}),f.on("autosave-disable-buttons.edit-post",function(){i.addClass("disabled")}).on("autosave-enable-buttons.edit-post",function(){wp.heartbeat&&wp.heartbeat.hasConnectionError()||i.removeClass("disabled")}).on("before-autosave.edit-post",function(){h(".autosave-message").text(x("Saving Draft\u2026"))}).on("after-autosave.edit-post",function(t,e){h(".autosave-message").text(e.message),h(document.body).hasClass("post-new-php")&&h(".submitbox .submitdelete").show()}),h(window).on("beforeunload.edit-post",function(t){var e=window.tinymce&&window.tinymce.get("content"),i=!1;if(wp.autosave?i=wp.autosave.server.postChanged():e&&(i=!e.isHidden()&&e.isDirty()),i)return t.preventDefault(),x("The changes you made will be lost if you navigate away from this page.")}).on("pagehide.edit-post",function(t){if(g&&(!t.target||"#document"==t.target.nodeName)){var t=h("#post_ID").val(),e=h("#active_post_lock").val();if(t&&e){t={action:"wp-remove-post-lock",_wpnonce:h("#_wpnonce").val(),post_ID:t,active_post_lock:e};if(window.FormData&&window.navigator.sendBeacon){var i=new window.FormData;if(h.each(t,function(t,e){i.append(t,e)}),window.navigator.sendBeacon(ajaxurl,i))return}h.post({async:!1,data:t,url:ajaxurl})}}}),h("#tagsdiv-post_tag").length?window.tagBox&&window.tagBox.init():h(".meta-box-sortables").children("div.postbox").each(function(){if(0===this.id.indexOf("tagsdiv-"))return window.tagBox&&window.tagBox.init(),!1}),h(".categorydiv").each(function(){var t,a,e,i=h(this).attr("id").split("-");i.shift(),a=i.join("-"),e="category"==a?"cats":a+"_tab",h("a","#"+a+"-tabs").on("click",function(t){t.preventDefault();t=h(this).attr("href");h(this).parent().addClass("tabs").siblings("li").removeClass("tabs"),h("#"+a+"-tabs").siblings(".tabs-panel").hide(),h(t).show(),"#"+a+"-all"==t?deleteUserSetting(e):setUserSetting(e,"pop")}),getUserSetting(e)&&h('a[href="#'+a+'-pop"]',"#"+a+"-tabs").trigger("click"),h("#new"+a).one("focus",function(){h(this).val("").removeClass("form-input-tip")}),h("#new"+a).on("keypress",function(t){13===t.keyCode&&(t.preventDefault(),h("#"+a+"-add-submit").trigger("click"))}),h("#"+a+"-add-submit").on("click",function(){h("#new"+a).trigger("focus")}),i=function(t){return!!h("#new"+a).val()&&(t.data+="&"+h(":checked","#"+a+"checklist").serialize(),h("#"+a+"-add-submit").prop("disabled",!0),t)},t=function(t,e){var i=h("#new"+a+"_parent");h("#"+a+"-add-submit").prop("disabled",!1),"undefined"!=e.parsed.responses[0]&&(e=e.parsed.responses[0].supplemental.newcat_parent)&&(i.before(e),i.remove())},h("#"+a+"checklist").wpList({alt:"",response:a+"-ajax-response",addBefore:i,addAfter:t}),h("#"+a+"-add-toggle").on("click",function(t){t.preventDefault(),h("#"+a+"-adder").toggleClass("wp-hidden-children"),h('a[href="#'+a+'-all"]',"#"+a+"-tabs").trigger("click"),h("#new"+a).trigger("focus")}),h("#"+a+"checklist, #"+a+"checklist-pop").on("click",'li.popular-category > label input[type="checkbox"]',function(){var t=h(this),e=t.is(":checked"),i=t.val();i&&t.parents("#taxonomy-"+a).length&&h("#in-"+a+"-"+i+", #in-popular-"+a+"-"+i).prop("checked",e)})}),h("#postcustom").length&&h("#the-list").wpList({addBefore:function(t){return t.data+="&post_id="+h("#post_ID").val(),t},addAfter:function(){h("table#list-table").show()}}),h("#submitdiv").length&&(p=h("#timestamp").html(),e=h("#post-visibility-display").html(),a=function(){"public"!=w.find("input:radio:checked").val()?(h("#sticky").prop("checked",!1),h("#sticky-span").hide()):h("#sticky-span").show(),"password"!=w.find("input:radio:checked").val()?h("#password-span").hide():h("#password-span").show()},n=function(){if(b.length){var t,e=h("#post_status"),i=h('option[value="publish"]',e),a=h("#aa").val(),n=h("#mm").val(),s=h("#jj").val(),o=h("#hh").val(),l=h("#mn").val(),r=new Date(a,n-1,s,o,l),c=new Date(h("#hidden_aa").val(),h("#hidden_mm").val()-1,h("#hidden_jj").val(),h("#hidden_hh").val(),h("#hidden_mn").val()),d=new Date(h("#cur_aa").val(),h("#cur_mm").val()-1,h("#cur_jj").val(),h("#cur_hh").val(),h("#cur_mn").val());if(r.getFullYear()!=a||1+r.getMonth()!=n||r.getDate()!=s||r.getMinutes()!=l)return b.find(".timestamp-wrap").addClass("form-invalid"),!1;b.find(".timestamp-wrap").removeClass("form-invalid"),d<r?(t=x("Schedule for:"),h("#publish").val(C("Schedule","post action/button label"))):r<=d&&"publish"!=h("#original_post_status").val()?(t=x("Publish on:"),h("#publish").val(x("Publish"))):(t=x("Published on:"),h("#publish").val(x("Update"))),c.toUTCString()==r.toUTCString()?h("#timestamp").html(p):h("#timestamp").html("\n"+t+" <b>"+x("%1$s %2$s, %3$s at %4$s:%5$s").replace("%1$s",h('option[value="'+n+'"]',"#mm").attr("data-text")).replace("%2$s",parseInt(s,10)).replace("%3$s",a).replace("%4$s",("00"+o).slice(-2)).replace("%5$s",("00"+l).slice(-2))+"</b> "),"private"==w.find("input:radio:checked").val()?(h("#publish").val(x("Update")),0===i.length?e.append('<option value="publish">'+x("Privately Published")+"</option>"):i.html(x("Privately Published")),h('option[value="publish"]',e).prop("selected",!0),h("#misc-publishing-actions .edit-post-status").hide()):("future"==h("#original_post_status").val()||"draft"==h("#original_post_status").val()?i.length&&(i.remove(),e.val(h("#hidden_post_status").val())):i.html(x("Published")),e.is(":hidden")&&h("#misc-publishing-actions .edit-post-status").show()),h("#post-status-display").text(wp.sanitize.stripTagsAndEncodeText(h("option:selected",e).text())),"private"==h("option:selected",e).val()||"publish"==h("option:selected",e).val()?h("#save-post").hide():(h("#save-post").show(),"pending"==h("option:selected",e).val()?h("#save-post").show().val(x("Save as Pending")):h("#save-post").show().val(x("Save Draft")))}return!0},h("#visibility .edit-visibility").on("click",function(t){t.preventDefault(),w.is(":hidden")&&(a(),w.slideDown("fast",function(){w.find('input[type="radio"]').first().trigger("focus")}),h(this).hide())}),w.find(".cancel-post-visibility").on("click",function(t){w.slideUp("fast"),h("#visibility-radio-"+h("#hidden-post-visibility").val()).prop("checked",!0),h("#post_password").val(h("#hidden-post-password").val()),h("#sticky").prop("checked",h("#hidden-post-sticky").prop("checked")),h("#post-visibility-display").html(e),h("#visibility .edit-visibility").show().trigger("focus"),n(),t.preventDefault()}),w.find(".save-post-visibility").on("click",function(t){var e="",i=w.find("input:radio:checked").val();switch(w.slideUp("fast"),h("#visibility .edit-visibility").show().trigger("focus"),n(),"public"!==i&&h("#sticky").prop("checked",!1),i){case"public":e=h("#sticky").prop("checked")?x("Public, Sticky"):x("Public");break;case"private":e=x("Private");break;case"password":e=x("Password Protected")}h("#post-visibility-display").text(e),t.preventDefault()}),w.find("input:radio").on("change",function(){a()}),b.siblings("a.edit-timestamp").on("click",function(t){b.is(":hidden")&&(b.slideDown("fast",function(){h("input, select",b.find(".timestamp-wrap")).first().trigger("focus")}),h(this).hide()),t.preventDefault()}),b.find(".cancel-timestamp").on("click",function(t){b.slideUp("fast").siblings("a.edit-timestamp").show().trigger("focus"),h("#mm").val(h("#hidden_mm").val()),h("#jj").val(h("#hidden_jj").val()),h("#aa").val(h("#hidden_aa").val()),h("#hh").val(h("#hidden_hh").val()),h("#mn").val(h("#hidden_mn").val()),n(),t.preventDefault()}),b.find(".save-timestamp").on("click",function(t){n()&&(b.slideUp("fast"),b.siblings("a.edit-timestamp").show().trigger("focus")),t.preventDefault()}),h("#post").on("submit",function(t){n()||(t.preventDefault(),b.show(),wp.autosave&&wp.autosave.enableButtons(),h("#publishing-action .spinner").removeClass("is-active"))}),k.siblings("a.edit-post-status").on("click",function(t){k.is(":hidden")&&(k.slideDown("fast",function(){k.find("select").trigger("focus")}),h(this).hide()),t.preventDefault()}),k.find(".save-post-status").on("click",function(t){k.slideUp("fast").siblings("a.edit-post-status").show().trigger("focus"),n(),t.preventDefault()}),k.find(".cancel-post-status").on("click",function(t){k.slideUp("fast").siblings("a.edit-post-status").show().trigger("focus"),h("#post_status").val(h("#hidden_post_status").val()),n(),t.preventDefault()})),h("#titlediv").on("click",".edit-slug",function(){var t,e,a,i,n=0,s=h("#post_name"),o=s.val(),l=h("#sample-permalink"),r=l.html(),c=h("#sample-permalink a").html(),d=h("#edit-slug-buttons"),p=d.html(),u=h("#editable-post-name-full");for(u.find("img").replaceWith(function(){return this.alt}),u=u.html(),l.html(c),a=h("#editable-post-name"),i=a.html(),d.html('<button type="button" class="save button button-small">'+x("OK")+'</button> <button type="button" class="cancel button-link">'+x("Cancel")+"</button>"),d.children(".save").on("click",function(){var i=a.children("input").val();i==h("#editable-post-name-full").text()?d.children(".cancel").trigger("click"):h.post(ajaxurl,{action:"sample-permalink",post_id:v,new_slug:i,new_title:h("#title").val(),samplepermalinknonce:h("#samplepermalinknonce").val()},function(t){var e=h("#edit-slug-box");e.html(t),e.hasClass("hidden")&&e.fadeIn("fast",function(){e.removeClass("hidden")}),d.html(p),l.html(r),s.val(i),h(".edit-slug").trigger("focus"),wp.a11y.speak(x("Permalink saved"))})}),d.children(".cancel").on("click",function(){h("#view-post-btn").show(),a.html(i),d.html(p),l.html(r),s.val(o),h(".edit-slug").trigger("focus")}),t=0;t<u.length;++t)"%"==u.charAt(t)&&n++;c=n>u.length/4?"":u,e=x("URL Slug"),a.html('<label for="new-post-slug" class="screen-reader-text">'+e+'</label><input type="text" id="new-post-slug" value="'+c+'" autocomplete="off" spellcheck="false" />').children("input").on("keydown",function(t){var e=t.which;13===e&&(t.preventDefault(),d.children(".save").trigger("click")),27===e&&d.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")),f.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(){d=(d=window.location.href)+(-1!==d.indexOf("?")?"&":"?")+"wp-post-new-reload=true",window.history.replaceState(null,null,d)}),y.on("success",function(t){var e=h(t.trigger),i=h(".success",e.closest(".copy-to-clipboard-container"));t.clearSelection(),e.trigger("focus"),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);
\ No newline at end of file diff --git a/wp-admin/js/postbox.js b/wp-admin/js/postbox.js new file mode 100644 index 0000000..c80866f --- /dev/null +++ b/wp-admin/js/postbox.js @@ -0,0 +1,654 @@ +/** + * Contains the postboxes logic, opening and closing postboxes, reordering and saving + * the state and ordering to the database. + * + * @since 2.5.0 + * @requires jQuery + * @output wp-admin/js/postbox.js + */ + +/* global ajaxurl, postboxes */ + +(function($) { + var $document = $( document ), + __ = wp.i18n.__; + + /** + * This object contains all function to handle the behavior of the post boxes. The post boxes are the boxes you see + * around the content on the edit page. + * + * @since 2.7.0 + * + * @namespace postboxes + * + * @type {Object} + */ + window.postboxes = { + + /** + * Handles a click on either the postbox heading or the postbox open/close icon. + * + * Opens or closes the postbox. Expects `this` to equal the clicked element. + * Calls postboxes.pbshow if the postbox has been opened, calls postboxes.pbhide + * if the postbox has been closed. + * + * @since 4.4.0 + * + * @memberof postboxes + * + * @fires postboxes#postbox-toggled + * + * @return {void} + */ + handle_click : function () { + var $el = $( this ), + p = $el.closest( '.postbox' ), + id = p.attr( 'id' ), + ariaExpandedValue; + + if ( 'dashboard_browser_nag' === id ) { + return; + } + + p.toggleClass( 'closed' ); + ariaExpandedValue = ! p.hasClass( 'closed' ); + + if ( $el.hasClass( 'handlediv' ) ) { + // The handle button was clicked. + $el.attr( 'aria-expanded', ariaExpandedValue ); + } else { + // The handle heading was clicked. + $el.closest( '.postbox' ).find( 'button.handlediv' ) + .attr( 'aria-expanded', ariaExpandedValue ); + } + + if ( postboxes.page !== 'press-this' ) { + postboxes.save_state( postboxes.page ); + } + + if ( id ) { + if ( !p.hasClass('closed') && typeof postboxes.pbshow === 'function' ) { + postboxes.pbshow( id ); + } else if ( p.hasClass('closed') && typeof postboxes.pbhide === 'function' ) { + postboxes.pbhide( id ); + } + } + + /** + * Fires when a postbox has been opened or closed. + * + * Contains a jQuery object with the relevant postbox element. + * + * @since 4.0.0 + * @ignore + * + * @event postboxes#postbox-toggled + * @type {Object} + */ + $document.trigger( 'postbox-toggled', p ); + }, + + /** + * Handles clicks on the move up/down buttons. + * + * @since 5.5.0 + * + * @return {void} + */ + handleOrder: function() { + var button = $( this ), + postbox = button.closest( '.postbox' ), + postboxId = postbox.attr( 'id' ), + postboxesWithinSortables = postbox.closest( '.meta-box-sortables' ).find( '.postbox:visible' ), + postboxesWithinSortablesCount = postboxesWithinSortables.length, + postboxWithinSortablesIndex = postboxesWithinSortables.index( postbox ), + firstOrLastPositionMessage; + + if ( 'dashboard_browser_nag' === postboxId ) { + return; + } + + // If on the first or last position, do nothing and send an audible message to screen reader users. + if ( 'true' === button.attr( 'aria-disabled' ) ) { + firstOrLastPositionMessage = button.hasClass( 'handle-order-higher' ) ? + __( 'The box is on the first position' ) : + __( 'The box is on the last position' ); + + wp.a11y.speak( firstOrLastPositionMessage ); + return; + } + + // Move a postbox up. + if ( button.hasClass( 'handle-order-higher' ) ) { + // If the box is first within a sortable area, move it to the previous sortable area. + if ( 0 === postboxWithinSortablesIndex ) { + postboxes.handleOrderBetweenSortables( 'previous', button, postbox ); + return; + } + + postbox.prevAll( '.postbox:visible' ).eq( 0 ).before( postbox ); + button.trigger( 'focus' ); + postboxes.updateOrderButtonsProperties(); + postboxes.save_order( postboxes.page ); + } + + // Move a postbox down. + if ( button.hasClass( 'handle-order-lower' ) ) { + // If the box is last within a sortable area, move it to the next sortable area. + if ( postboxWithinSortablesIndex + 1 === postboxesWithinSortablesCount ) { + postboxes.handleOrderBetweenSortables( 'next', button, postbox ); + return; + } + + postbox.nextAll( '.postbox:visible' ).eq( 0 ).after( postbox ); + button.trigger( 'focus' ); + postboxes.updateOrderButtonsProperties(); + postboxes.save_order( postboxes.page ); + } + + }, + + /** + * Moves postboxes between the sortables areas. + * + * @since 5.5.0 + * + * @param {string} position The "previous" or "next" sortables area. + * @param {Object} button The jQuery object representing the button that was clicked. + * @param {Object} postbox The jQuery object representing the postbox to be moved. + * + * @return {void} + */ + handleOrderBetweenSortables: function( position, button, postbox ) { + var closestSortablesId = button.closest( '.meta-box-sortables' ).attr( 'id' ), + sortablesIds = [], + sortablesIndex, + detachedPostbox; + + // Get the list of sortables within the page. + $( '.meta-box-sortables:visible' ).each( function() { + sortablesIds.push( $( this ).attr( 'id' ) ); + }); + + // Return if there's only one visible sortables area, e.g. in the block editor page. + if ( 1 === sortablesIds.length ) { + return; + } + + // Find the index of the current sortables area within all the sortable areas. + sortablesIndex = $.inArray( closestSortablesId, sortablesIds ); + // Detach the postbox to be moved. + detachedPostbox = postbox.detach(); + + // Move the detached postbox to its new position. + if ( 'previous' === position ) { + $( detachedPostbox ).appendTo( '#' + sortablesIds[ sortablesIndex - 1 ] ); + } + + if ( 'next' === position ) { + $( detachedPostbox ).prependTo( '#' + sortablesIds[ sortablesIndex + 1 ] ); + } + + postboxes._mark_area(); + button.focus(); + postboxes.updateOrderButtonsProperties(); + postboxes.save_order( postboxes.page ); + }, + + /** + * Update the move buttons properties depending on the postbox position. + * + * @since 5.5.0 + * + * @return {void} + */ + updateOrderButtonsProperties: function() { + var firstSortablesId = $( '.meta-box-sortables:visible:first' ).attr( 'id' ), + lastSortablesId = $( '.meta-box-sortables:visible:last' ).attr( 'id' ), + firstPostbox = $( '.postbox:visible:first' ), + lastPostbox = $( '.postbox:visible:last' ), + firstPostboxId = firstPostbox.attr( 'id' ), + lastPostboxId = lastPostbox.attr( 'id' ), + firstPostboxSortablesId = firstPostbox.closest( '.meta-box-sortables' ).attr( 'id' ), + lastPostboxSortablesId = lastPostbox.closest( '.meta-box-sortables' ).attr( 'id' ), + moveUpButtons = $( '.handle-order-higher' ), + moveDownButtons = $( '.handle-order-lower' ); + + // Enable all buttons as a reset first. + moveUpButtons + .attr( 'aria-disabled', 'false' ) + .removeClass( 'hidden' ); + moveDownButtons + .attr( 'aria-disabled', 'false' ) + .removeClass( 'hidden' ); + + // When there's only one "sortables" area (e.g. in the block editor) and only one visible postbox, hide the buttons. + if ( firstSortablesId === lastSortablesId && firstPostboxId === lastPostboxId ) { + moveUpButtons.addClass( 'hidden' ); + moveDownButtons.addClass( 'hidden' ); + } + + // Set an aria-disabled=true attribute on the first visible "move" buttons. + if ( firstSortablesId === firstPostboxSortablesId ) { + $( firstPostbox ).find( '.handle-order-higher' ).attr( 'aria-disabled', 'true' ); + } + + // Set an aria-disabled=true attribute on the last visible "move" buttons. + if ( lastSortablesId === lastPostboxSortablesId ) { + $( '.postbox:visible .handle-order-lower' ).last().attr( 'aria-disabled', 'true' ); + } + }, + + /** + * Adds event handlers to all postboxes and screen option on the current page. + * + * @since 2.7.0 + * + * @memberof postboxes + * + * @param {string} page The page we are currently on. + * @param {Object} [args] + * @param {Function} args.pbshow A callback that is called when a postbox opens. + * @param {Function} args.pbhide A callback that is called when a postbox closes. + * @return {void} + */ + add_postbox_toggles : function (page, args) { + var $handles = $( '.postbox .hndle, .postbox .handlediv' ), + $orderButtons = $( '.postbox .handle-order-higher, .postbox .handle-order-lower' ); + + this.page = page; + this.init( page, args ); + + $handles.on( 'click.postboxes', this.handle_click ); + + // Handle the order of the postboxes. + $orderButtons.on( 'click.postboxes', this.handleOrder ); + + /** + * @since 2.7.0 + */ + $('.postbox .hndle a').on( 'click', function(e) { + e.stopPropagation(); + }); + + /** + * Hides a postbox. + * + * Event handler for the postbox dismiss button. After clicking the button + * the postbox will be hidden. + * + * As of WordPress 5.5, this is only used for the browser update nag. + * + * @since 3.2.0 + * + * @return {void} + */ + $( '.postbox a.dismiss' ).on( 'click.postboxes', function( e ) { + var hide_id = $(this).parents('.postbox').attr('id') + '-hide'; + e.preventDefault(); + $( '#' + hide_id ).prop('checked', false).triggerHandler('click'); + }); + + /** + * Hides the postbox element + * + * Event handler for the screen options checkboxes. When a checkbox is + * clicked this function will hide or show the relevant postboxes. + * + * @since 2.7.0 + * @ignore + * + * @fires postboxes#postbox-toggled + * + * @return {void} + */ + $('.hide-postbox-tog').on('click.postboxes', function() { + var $el = $(this), + boxId = $el.val(), + $postbox = $( '#' + boxId ); + + if ( $el.prop( 'checked' ) ) { + $postbox.show(); + if ( typeof postboxes.pbshow === 'function' ) { + postboxes.pbshow( boxId ); + } + } else { + $postbox.hide(); + if ( typeof postboxes.pbhide === 'function' ) { + postboxes.pbhide( boxId ); + } + } + + postboxes.save_state( page ); + postboxes._mark_area(); + + /** + * @since 4.0.0 + * @see postboxes.handle_click + */ + $document.trigger( 'postbox-toggled', $postbox ); + }); + + /** + * Changes the amount of columns based on the layout preferences. + * + * @since 2.8.0 + * + * @return {void} + */ + $('.columns-prefs input[type="radio"]').on('click.postboxes', function(){ + var n = parseInt($(this).val(), 10); + + if ( n ) { + postboxes._pb_edit(n); + postboxes.save_order( page ); + } + }); + }, + + /** + * Initializes all the postboxes, mainly their sortable behavior. + * + * @since 2.7.0 + * + * @memberof postboxes + * + * @param {string} page The page we are currently on. + * @param {Object} [args={}] The arguments for the postbox initializer. + * @param {Function} args.pbshow A callback that is called when a postbox opens. + * @param {Function} args.pbhide A callback that is called when a postbox + * closes. + * + * @return {void} + */ + init : function(page, args) { + var isMobile = $( document.body ).hasClass( 'mobile' ), + $handleButtons = $( '.postbox .handlediv' ); + + $.extend( this, args || {} ); + $('.meta-box-sortables').sortable({ + placeholder: 'sortable-placeholder', + connectWith: '.meta-box-sortables', + items: '.postbox', + handle: '.hndle', + cursor: 'move', + delay: ( isMobile ? 200 : 0 ), + distance: 2, + tolerance: 'pointer', + forcePlaceholderSize: true, + helper: function( event, element ) { + /* `helper: 'clone'` is equivalent to `return element.clone();` + * Cloning a checked radio and then inserting that clone next to the original + * radio unchecks the original radio (since only one of the two can be checked). + * We get around this by renaming the helper's inputs' name attributes so that, + * when the helper is inserted into the DOM for the sortable, no radios are + * duplicated, and no original radio gets unchecked. + */ + return element.clone() + .find( ':input' ) + .attr( 'name', function( i, currentName ) { + return 'sort_' + parseInt( Math.random() * 100000, 10 ).toString() + '_' + currentName; + } ) + .end(); + }, + opacity: 0.65, + start: function() { + $( 'body' ).addClass( 'is-dragging-metaboxes' ); + // Refresh the cached positions of all the sortable items so that the min-height set while dragging works. + $( '.meta-box-sortables' ).sortable( 'refreshPositions' ); + }, + stop: function() { + var $el = $( this ); + + $( 'body' ).removeClass( 'is-dragging-metaboxes' ); + + if ( $el.find( '#dashboard_browser_nag' ).is( ':visible' ) && 'dashboard_browser_nag' != this.firstChild.id ) { + $el.sortable('cancel'); + return; + } + + postboxes.updateOrderButtonsProperties(); + postboxes.save_order(page); + }, + receive: function(e,ui) { + if ( 'dashboard_browser_nag' == ui.item[0].id ) + $(ui.sender).sortable('cancel'); + + postboxes._mark_area(); + $document.trigger( 'postbox-moved', ui.item ); + } + }); + + if ( isMobile ) { + $(document.body).on('orientationchange.postboxes', function(){ postboxes._pb_change(); }); + this._pb_change(); + } + + this._mark_area(); + + // Update the "move" buttons properties. + this.updateOrderButtonsProperties(); + $document.on( 'postbox-toggled', this.updateOrderButtonsProperties ); + + // Set the handle buttons `aria-expanded` attribute initial value on page load. + $handleButtons.each( function () { + var $el = $( this ); + $el.attr( 'aria-expanded', ! $el.closest( '.postbox' ).hasClass( 'closed' ) ); + }); + }, + + /** + * Saves the state of the postboxes to the server. + * + * It sends two lists, one with all the closed postboxes, one with all the + * hidden postboxes. + * + * @since 2.7.0 + * + * @memberof postboxes + * + * @param {string} page The page we are currently on. + * @return {void} + */ + save_state : function(page) { + var closed, hidden; + + // Return on the nav-menus.php screen, see #35112. + if ( 'nav-menus' === page ) { + return; + } + + closed = $( '.postbox' ).filter( '.closed' ).map( function() { return this.id; } ).get().join( ',' ); + hidden = $( '.postbox' ).filter( ':hidden' ).map( function() { return this.id; } ).get().join( ',' ); + + $.post(ajaxurl, { + action: 'closed-postboxes', + closed: closed, + hidden: hidden, + closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(), + page: page + }); + }, + + /** + * Saves the order of the postboxes to the server. + * + * Sends a list of all postboxes inside a sortable area to the server. + * + * @since 2.8.0 + * + * @memberof postboxes + * + * @param {string} page The page we are currently on. + * @return {void} + */ + save_order : function(page) { + var postVars, page_columns = $('.columns-prefs input:checked').val() || 0; + + postVars = { + action: 'meta-box-order', + _ajax_nonce: $('#meta-box-order-nonce').val(), + page_columns: page_columns, + page: page + }; + + $('.meta-box-sortables').each( function() { + postVars[ 'order[' + this.id.split( '-' )[0] + ']' ] = $( this ).sortable( 'toArray' ).join( ',' ); + } ); + + $.post( + ajaxurl, + postVars, + function( response ) { + if ( response.success ) { + wp.a11y.speak( __( 'The boxes order has been saved.' ) ); + } + } + ); + }, + + /** + * Marks empty postbox areas. + * + * Adds a message to empty sortable areas on the dashboard page. Also adds a + * border around the side area on the post edit screen if there are no postboxes + * present. + * + * @since 3.3.0 + * @access private + * + * @memberof postboxes + * + * @return {void} + */ + _mark_area : function() { + var visible = $( 'div.postbox:visible' ).length, + visibleSortables = $( '#dashboard-widgets .meta-box-sortables:visible, #post-body .meta-box-sortables:visible' ), + areAllVisibleSortablesEmpty = true; + + visibleSortables.each( function() { + var t = $(this); + + if ( visible == 1 || t.children( '.postbox:visible' ).length ) { + t.removeClass('empty-container'); + areAllVisibleSortablesEmpty = false; + } + else { + t.addClass('empty-container'); + } + }); + + postboxes.updateEmptySortablesText( visibleSortables, areAllVisibleSortablesEmpty ); + }, + + /** + * Updates the text for the empty sortable areas on the Dashboard. + * + * @since 5.5.0 + * + * @param {Object} visibleSortables The jQuery object representing the visible sortable areas. + * @param {boolean} areAllVisibleSortablesEmpty Whether all the visible sortable areas are "empty". + * + * @return {void} + */ + updateEmptySortablesText: function( visibleSortables, areAllVisibleSortablesEmpty ) { + var isDashboard = $( '#dashboard-widgets' ).length, + emptySortableText = areAllVisibleSortablesEmpty ? __( 'Add boxes from the Screen Options menu' ) : __( 'Drag boxes here' ); + + if ( ! isDashboard ) { + return; + } + + visibleSortables.each( function() { + if ( $( this ).hasClass( 'empty-container' ) ) { + $( this ).attr( 'data-emptyString', emptySortableText ); + } + } ); + }, + + /** + * Changes the amount of columns on the post edit page. + * + * @since 3.3.0 + * @access private + * + * @memberof postboxes + * + * @fires postboxes#postboxes-columnchange + * + * @param {number} n The amount of columns to divide the post edit page in. + * @return {void} + */ + _pb_edit : function(n) { + var el = $('.metabox-holder').get(0); + + if ( el ) { + el.className = el.className.replace(/columns-\d+/, 'columns-' + n); + } + + /** + * Fires when the amount of columns on the post edit page has been changed. + * + * @since 4.0.0 + * @ignore + * + * @event postboxes#postboxes-columnchange + */ + $( document ).trigger( 'postboxes-columnchange' ); + }, + + /** + * Changes the amount of columns the postboxes are in based on the current + * orientation of the browser. + * + * @since 3.3.0 + * @access private + * + * @memberof postboxes + * + * @return {void} + */ + _pb_change : function() { + var check = $( 'label.columns-prefs-1 input[type="radio"]' ); + + switch ( window.orientation ) { + case 90: + case -90: + if ( !check.length || !check.is(':checked') ) + this._pb_edit(2); + break; + case 0: + case 180: + if ( $( '#poststuff' ).length ) { + this._pb_edit(1); + } else { + if ( !check.length || !check.is(':checked') ) + this._pb_edit(2); + } + break; + } + }, + + /* Callbacks */ + + /** + * @since 2.7.0 + * @access public + * + * @property {Function|boolean} pbshow A callback that is called when a postbox + * is opened. + * @memberof postboxes + */ + pbshow : false, + + /** + * @since 2.7.0 + * @access public + * @property {Function|boolean} pbhide A callback that is called when a postbox + * is closed. + * @memberof postboxes + */ + pbhide : false + }; + +}(jQuery)); diff --git a/wp-admin/js/postbox.min.js b/wp-admin/js/postbox.min.js new file mode 100644 index 0000000..609a109 --- /dev/null +++ b/wp-admin/js/postbox.min.js @@ -0,0 +1,2 @@ +/*! 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);
\ No newline at end of file diff --git a/wp-admin/js/privacy-tools.js b/wp-admin/js/privacy-tools.js new file mode 100644 index 0000000..e5fceb8 --- /dev/null +++ b/wp-admin/js/privacy-tools.js @@ -0,0 +1,346 @@ +/** + * Interactions used by the User Privacy tools in WordPress. + * + * @output wp-admin/js/privacy-tools.js + */ + +// Privacy request action handling. +jQuery( function( $ ) { + var __ = wp.i18n.__, + copiedNoticeTimeout; + + function setActionState( $action, state ) { + $action.children().addClass( 'hidden' ); + $action.children( '.' + state ).removeClass( 'hidden' ); + } + + function clearResultsAfterRow( $requestRow ) { + $requestRow.removeClass( 'has-request-results' ); + + if ( $requestRow.next().hasClass( 'request-results' ) ) { + $requestRow.next().remove(); + } + } + + function appendResultsAfterRow( $requestRow, classes, summaryMessage, additionalMessages ) { + var itemList = '', + resultRowClasses = 'request-results'; + + clearResultsAfterRow( $requestRow ); + + if ( additionalMessages.length ) { + $.each( additionalMessages, function( index, value ) { + itemList = itemList + '<li>' + value + '</li>'; + }); + itemList = '<ul>' + itemList + '</ul>'; + } + + $requestRow.addClass( 'has-request-results' ); + + if ( $requestRow.hasClass( 'status-request-confirmed' ) ) { + resultRowClasses = resultRowClasses + ' status-request-confirmed'; + } + + if ( $requestRow.hasClass( 'status-request-failed' ) ) { + resultRowClasses = resultRowClasses + ' status-request-failed'; + } + + $requestRow.after( function() { + return '<tr class="' + resultRowClasses + '"><th colspan="5">' + + '<div class="notice inline notice-alt ' + classes + '">' + + '<p>' + summaryMessage + '</p>' + + itemList + + '</div>' + + '</td>' + + '</tr>'; + }); + } + + $( '.export-personal-data-handle' ).on( 'click', function( event ) { + var $this = $( this ), + $action = $this.parents( '.export-personal-data' ), + $requestRow = $this.parents( 'tr' ), + $progress = $requestRow.find( '.export-progress' ), + $rowActions = $this.parents( '.row-actions' ), + requestID = $action.data( 'request-id' ), + nonce = $action.data( 'nonce' ), + exportersCount = $action.data( 'exporters-count' ), + sendAsEmail = $action.data( 'send-as-email' ) ? true : false; + + event.preventDefault(); + event.stopPropagation(); + + $rowActions.addClass( 'processing' ); + + $action.trigger( 'blur' ); + clearResultsAfterRow( $requestRow ); + setExportProgress( 0 ); + + function onExportDoneSuccess( zipUrl ) { + var summaryMessage = __( 'This user’s personal data export link was sent.' ); + + if ( 'undefined' !== typeof zipUrl ) { + summaryMessage = __( 'This user’s personal data export file was downloaded.' ); + } + + setActionState( $action, 'export-personal-data-success' ); + + appendResultsAfterRow( $requestRow, 'notice-success', summaryMessage, [] ); + + if ( 'undefined' !== typeof zipUrl ) { + window.location = zipUrl; + } else if ( ! sendAsEmail ) { + onExportFailure( __( 'No personal data export file was generated.' ) ); + } + + setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); + } + + function onExportFailure( errorMessage ) { + var summaryMessage = __( 'An error occurred while attempting to export personal data.' ); + + setActionState( $action, 'export-personal-data-failed' ); + + if ( errorMessage ) { + appendResultsAfterRow( $requestRow, 'notice-error', summaryMessage, [ errorMessage ] ); + } + + setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); + } + + function setExportProgress( exporterIndex ) { + var progress = ( exportersCount > 0 ? exporterIndex / exportersCount : 0 ), + progressString = Math.round( progress * 100 ).toString() + '%'; + + $progress.html( progressString ); + } + + function doNextExport( exporterIndex, pageIndex ) { + $.ajax( + { + url: window.ajaxurl, + data: { + action: 'wp-privacy-export-personal-data', + exporter: exporterIndex, + id: requestID, + page: pageIndex, + security: nonce, + sendAsEmail: sendAsEmail + }, + method: 'post' + } + ).done( function( response ) { + var responseData = response.data; + + if ( ! response.success ) { + // e.g. invalid request ID. + setTimeout( function() { onExportFailure( response.data ); }, 500 ); + return; + } + + if ( ! responseData.done ) { + setTimeout( doNextExport( exporterIndex, pageIndex + 1 ) ); + } else { + setExportProgress( exporterIndex ); + if ( exporterIndex < exportersCount ) { + setTimeout( doNextExport( exporterIndex + 1, 1 ) ); + } else { + setTimeout( function() { onExportDoneSuccess( responseData.url ); }, 500 ); + } + } + }).fail( function( jqxhr, textStatus, error ) { + // e.g. Nonce failure. + setTimeout( function() { onExportFailure( error ); }, 500 ); + }); + } + + // And now, let's begin. + setActionState( $action, 'export-personal-data-processing' ); + doNextExport( 1, 1 ); + }); + + $( '.remove-personal-data-handle' ).on( 'click', function( event ) { + var $this = $( this ), + $action = $this.parents( '.remove-personal-data' ), + $requestRow = $this.parents( 'tr' ), + $progress = $requestRow.find( '.erasure-progress' ), + $rowActions = $this.parents( '.row-actions' ), + requestID = $action.data( 'request-id' ), + nonce = $action.data( 'nonce' ), + erasersCount = $action.data( 'erasers-count' ), + hasRemoved = false, + hasRetained = false, + messages = []; + + event.preventDefault(); + event.stopPropagation(); + + $rowActions.addClass( 'processing' ); + + $action.trigger( 'blur' ); + clearResultsAfterRow( $requestRow ); + setErasureProgress( 0 ); + + function onErasureDoneSuccess() { + var summaryMessage = __( 'No personal data was found for this user.' ), + classes = 'notice-success'; + + setActionState( $action, 'remove-personal-data-success' ); + + if ( false === hasRemoved ) { + if ( false === hasRetained ) { + summaryMessage = __( 'No personal data was found for this user.' ); + } else { + summaryMessage = __( 'Personal data was found for this user but was not erased.' ); + classes = 'notice-warning'; + } + } else { + if ( false === hasRetained ) { + summaryMessage = __( 'All of the personal data found for this user was erased.' ); + } else { + summaryMessage = __( 'Personal data was found for this user but some of the personal data found was not erased.' ); + classes = 'notice-warning'; + } + } + appendResultsAfterRow( $requestRow, classes, summaryMessage, messages ); + + setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); + } + + function onErasureFailure() { + var summaryMessage = __( 'An error occurred while attempting to find and erase personal data.' ); + + setActionState( $action, 'remove-personal-data-failed' ); + + appendResultsAfterRow( $requestRow, 'notice-error', summaryMessage, [] ); + + setTimeout( function() { $rowActions.removeClass( 'processing' ); }, 500 ); + } + + function setErasureProgress( eraserIndex ) { + var progress = ( erasersCount > 0 ? eraserIndex / erasersCount : 0 ), + progressString = Math.round( progress * 100 ).toString() + '%'; + + $progress.html( progressString ); + } + + function doNextErasure( eraserIndex, pageIndex ) { + $.ajax({ + url: window.ajaxurl, + data: { + action: 'wp-privacy-erase-personal-data', + eraser: eraserIndex, + id: requestID, + page: pageIndex, + security: nonce + }, + method: 'post' + }).done( function( response ) { + var responseData = response.data; + + if ( ! response.success ) { + setTimeout( function() { onErasureFailure(); }, 500 ); + return; + } + if ( responseData.items_removed ) { + hasRemoved = hasRemoved || responseData.items_removed; + } + if ( responseData.items_retained ) { + hasRetained = hasRetained || responseData.items_retained; + } + if ( responseData.messages ) { + messages = messages.concat( responseData.messages ); + } + if ( ! responseData.done ) { + setTimeout( doNextErasure( eraserIndex, pageIndex + 1 ) ); + } else { + setErasureProgress( eraserIndex ); + if ( eraserIndex < erasersCount ) { + setTimeout( doNextErasure( eraserIndex + 1, 1 ) ); + } else { + setTimeout( function() { onErasureDoneSuccess(); }, 500 ); + } + } + }).fail( function() { + setTimeout( function() { onErasureFailure(); }, 500 ); + }); + } + + // And now, let's begin. + setActionState( $action, 'remove-personal-data-processing' ); + + doNextErasure( 1, 1 ); + }); + + // Privacy Policy page, copy action. + $( document ).on( 'click', function( event ) { + var $parent, + range, + $target = $( event.target ), + copiedNotice = $target.siblings( '.success' ); + + clearTimeout( copiedNoticeTimeout ); + + if ( $target.is( 'button.privacy-text-copy' ) ) { + $parent = $target.closest( '.privacy-settings-accordion-panel' ); + + if ( $parent.length ) { + try { + var documentPosition = document.documentElement.scrollTop, + bodyPosition = document.body.scrollTop; + + // Setup copy. + window.getSelection().removeAllRanges(); + + // Hide tutorial content to remove from copied content. + range = document.createRange(); + $parent.addClass( 'hide-privacy-policy-tutorial' ); + + // Copy action. + range.selectNodeContents( $parent[0] ); + window.getSelection().addRange( range ); + document.execCommand( 'copy' ); + + // Reset section. + $parent.removeClass( 'hide-privacy-policy-tutorial' ); + window.getSelection().removeAllRanges(); + + // Return scroll position - see #49540. + if ( documentPosition > 0 && documentPosition !== document.documentElement.scrollTop ) { + document.documentElement.scrollTop = documentPosition; + } else if ( bodyPosition > 0 && bodyPosition !== document.body.scrollTop ) { + document.body.scrollTop = bodyPosition; + } + + // Display and speak notice to indicate action complete. + copiedNotice.addClass( 'visible' ); + wp.a11y.speak( __( 'The suggested policy text has been copied to your clipboard.' ) ); + + // Delay notice dismissal. + copiedNoticeTimeout = setTimeout( function() { + copiedNotice.removeClass( 'visible' ); + }, 3000 ); + } catch ( er ) {} + } + } + }); + + // Label handling to focus the create page button on Privacy settings page. + $( 'body.options-privacy-php label[for=create-page]' ).on( 'click', function( e ) { + e.preventDefault(); + $( 'input#create-page' ).trigger( 'focus' ); + } ); + + // Accordion handling in various new Privacy settings pages. + $( '.privacy-settings-accordion' ).on( 'click', '.privacy-settings-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 ); + } + } ); +}); diff --git a/wp-admin/js/privacy-tools.min.js b/wp-admin/js/privacy-tools.min.js new file mode 100644 index 0000000..41596c3 --- /dev/null +++ b/wp-admin/js/privacy-tools.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(v){var r,h=wp.i18n.__;function w(e,t){e.children().addClass("hidden"),e.children("."+t).removeClass("hidden")}function x(e){e.removeClass("has-request-results"),e.next().hasClass("request-results")&&e.next().remove()}function T(e,t,a,o){var s="",n="request-results";x(e),o.length&&(v.each(o,function(e,t){s=s+"<li>"+t+"</li>"}),s="<ul>"+s+"</ul>"),e.addClass("has-request-results"),e.hasClass("status-request-confirmed")&&(n+=" status-request-confirmed"),e.hasClass("status-request-failed")&&(n+=" status-request-failed"),e.after(function(){return'<tr class="'+n+'"><th colspan="5"><div class="notice inline notice-alt '+t+'"><p>'+a+"</p>"+s+"</div></td></tr>"})}v(".export-personal-data-handle").on("click",function(e){var t=v(this),n=t.parents(".export-personal-data"),r=t.parents("tr"),a=r.find(".export-progress"),i=t.parents(".row-actions"),c=n.data("request-id"),d=n.data("nonce"),l=n.data("exporters-count"),u=!!n.data("send-as-email");function p(e){var t=h("An error occurred while attempting to export personal data.");w(n,"export-personal-data-failed"),e&&T(r,"notice-error",t,[e]),setTimeout(function(){i.removeClass("processing")},500)}function m(e){e=Math.round(100*(0<l?e/l:0)).toString()+"%";a.html(e)}e.preventDefault(),e.stopPropagation(),i.addClass("processing"),n.trigger("blur"),x(r),m(0),w(n,"export-personal-data-processing"),function t(o,s){v.ajax({url:window.ajaxurl,data:{action:"wp-privacy-export-personal-data",exporter:o,id:c,page:s,security:d,sendAsEmail:u},method:"post"}).done(function(e){var a=e.data;e.success?a.done?(m(o),o<l?setTimeout(t(o+1,1)):setTimeout(function(){var e,t;e=a.url,t=h("This user’s personal data export link was sent."),void 0!==e&&(t=h("This user’s personal data export file was downloaded.")),w(n,"export-personal-data-success"),T(r,"notice-success",t,[]),void 0!==e?window.location=e:u||p(h("No personal data export file was generated.")),setTimeout(function(){i.removeClass("processing")},500)},500)):setTimeout(t(o,s+1)):setTimeout(function(){p(e.data)},500)}).fail(function(e,t,a){setTimeout(function(){p(a)},500)})}(1,1)}),v(".remove-personal-data-handle").on("click",function(e){var t=v(this),n=t.parents(".remove-personal-data"),r=t.parents("tr"),a=r.find(".erasure-progress"),i=t.parents(".row-actions"),c=n.data("request-id"),d=n.data("nonce"),l=n.data("erasers-count"),u=!1,p=!1,m=[];function f(){var e=h("An error occurred while attempting to find and erase personal data.");w(n,"remove-personal-data-failed"),T(r,"notice-error",e,[]),setTimeout(function(){i.removeClass("processing")},500)}function g(e){e=Math.round(100*(0<l?e/l:0)).toString()+"%";a.html(e)}e.preventDefault(),e.stopPropagation(),i.addClass("processing"),n.trigger("blur"),x(r),g(0),w(n,"remove-personal-data-processing"),function a(o,s){v.ajax({url:window.ajaxurl,data:{action:"wp-privacy-erase-personal-data",eraser:o,id:c,page:s,security:d},method:"post"}).done(function(e){var t=e.data;e.success?(t.items_removed&&(u=u||t.items_removed),t.items_retained&&(p=p||t.items_retained),t.messages&&(m=m.concat(t.messages)),t.done?(g(o),o<l?setTimeout(a(o+1,1)):setTimeout(function(){var e,t;e=h("No personal data was found for this user."),t="notice-success",w(n,"remove-personal-data-success"),!1===u?!1===p?e=h("No personal data was found for this user."):(e=h("Personal data was found for this user but was not erased."),t="notice-warning"):!1===p?e=h("All of the personal data found for this user was erased."):(e=h("Personal data was found for this user but some of the personal data found was not erased."),t="notice-warning"),T(r,t,e,m),setTimeout(function(){i.removeClass("processing")},500)},500)):setTimeout(a(o,s+1))):setTimeout(function(){f()},500)}).fail(function(){setTimeout(function(){f()},500)})}(1,1)}),v(document).on("click",function(e){var t,a,e=v(e.target),o=e.siblings(".success");if(clearTimeout(r),e.is("button.privacy-text-copy")&&(t=e.closest(".privacy-settings-accordion-panel")).length)try{var s=document.documentElement.scrollTop,n=document.body.scrollTop;window.getSelection().removeAllRanges(),a=document.createRange(),t.addClass("hide-privacy-policy-tutorial"),a.selectNodeContents(t[0]),window.getSelection().addRange(a),document.execCommand("copy"),t.removeClass("hide-privacy-policy-tutorial"),window.getSelection().removeAllRanges(),0<s&&s!==document.documentElement.scrollTop?document.documentElement.scrollTop=s:0<n&&n!==document.body.scrollTop&&(document.body.scrollTop=n),o.addClass("visible"),wp.a11y.speak(h("The suggested policy text has been copied to your clipboard.")),r=setTimeout(function(){o.removeClass("visible")},3e3)}catch(e){}}),v("body.options-privacy-php label[for=create-page]").on("click",function(e){e.preventDefault(),v("input#create-page").trigger("focus")}),v(".privacy-settings-accordion").on("click",".privacy-settings-accordion-trigger",function(){"true"===v(this).attr("aria-expanded")?(v(this).attr("aria-expanded","false"),v("#"+v(this).attr("aria-controls")).attr("hidden",!0)):(v(this).attr("aria-expanded","true"),v("#"+v(this).attr("aria-controls")).attr("hidden",!1))})});
\ No newline at end of file diff --git a/wp-admin/js/revisions.js b/wp-admin/js/revisions.js new file mode 100644 index 0000000..83c2641 --- /dev/null +++ b/wp-admin/js/revisions.js @@ -0,0 +1,1175 @@ +/** + * @file Revisions interface functions, Backbone classes and + * the revisions.php document.ready bootstrap. + * + * @output wp-admin/js/revisions.js + */ + +/* global isRtl */ + +window.wp = window.wp || {}; + +(function($) { + var revisions; + /** + * Expose the module in window.wp.revisions. + */ + revisions = wp.revisions = { model: {}, view: {}, controller: {} }; + + // Link post revisions data served from the back end. + revisions.settings = window._wpRevisionsSettings || {}; + + // For debugging. + revisions.debug = false; + + /** + * wp.revisions.log + * + * A debugging utility for revisions. Works only when a + * debug flag is on and the browser supports it. + */ + revisions.log = function() { + if ( window.console && revisions.debug ) { + window.console.log.apply( window.console, arguments ); + } + }; + + // Handy functions to help with positioning. + $.fn.allOffsets = function() { + var offset = this.offset() || {top: 0, left: 0}, win = $(window); + return _.extend( offset, { + right: win.width() - offset.left - this.outerWidth(), + bottom: win.height() - offset.top - this.outerHeight() + }); + }; + + $.fn.allPositions = function() { + var position = this.position() || {top: 0, left: 0}, parent = this.parent(); + return _.extend( position, { + right: parent.outerWidth() - position.left - this.outerWidth(), + bottom: parent.outerHeight() - position.top - this.outerHeight() + }); + }; + + /** + * ======================================================================== + * MODELS + * ======================================================================== + */ + revisions.model.Slider = Backbone.Model.extend({ + defaults: { + value: null, + values: null, + min: 0, + max: 1, + step: 1, + range: false, + compareTwoMode: false + }, + + initialize: function( options ) { + this.frame = options.frame; + this.revisions = options.revisions; + + // Listen for changes to the revisions or mode from outside. + this.listenTo( this.frame, 'update:revisions', this.receiveRevisions ); + this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode ); + + // Listen for internal changes. + this.on( 'change:from', this.handleLocalChanges ); + this.on( 'change:to', this.handleLocalChanges ); + this.on( 'change:compareTwoMode', this.updateSliderSettings ); + this.on( 'update:revisions', this.updateSliderSettings ); + + // Listen for changes to the hovered revision. + this.on( 'change:hoveredRevision', this.hoverRevision ); + + this.set({ + max: this.revisions.length - 1, + compareTwoMode: this.frame.get('compareTwoMode'), + from: this.frame.get('from'), + to: this.frame.get('to') + }); + this.updateSliderSettings(); + }, + + getSliderValue: function( a, b ) { + return isRtl ? this.revisions.length - this.revisions.indexOf( this.get(a) ) - 1 : this.revisions.indexOf( this.get(b) ); + }, + + updateSliderSettings: function() { + if ( this.get('compareTwoMode') ) { + this.set({ + values: [ + this.getSliderValue( 'to', 'from' ), + this.getSliderValue( 'from', 'to' ) + ], + value: null, + range: true // Ensures handles cannot cross. + }); + } else { + this.set({ + value: this.getSliderValue( 'to', 'to' ), + values: null, + range: false + }); + } + this.trigger( 'update:slider' ); + }, + + // Called when a revision is hovered. + hoverRevision: function( model, value ) { + this.trigger( 'hovered:revision', value ); + }, + + // Called when `compareTwoMode` changes. + updateMode: function( model, value ) { + this.set({ compareTwoMode: value }); + }, + + // Called when `from` or `to` changes in the local model. + handleLocalChanges: function() { + this.frame.set({ + from: this.get('from'), + to: this.get('to') + }); + }, + + // Receives revisions changes from outside the model. + receiveRevisions: function( from, to ) { + // Bail if nothing changed. + if ( this.get('from') === from && this.get('to') === to ) { + return; + } + + this.set({ from: from, to: to }, { silent: true }); + this.trigger( 'update:revisions', from, to ); + } + + }); + + revisions.model.Tooltip = Backbone.Model.extend({ + defaults: { + revision: null, + offset: {}, + hovering: false, // Whether the mouse is hovering. + scrubbing: false // Whether the mouse is scrubbing. + }, + + initialize: function( options ) { + this.frame = options.frame; + this.revisions = options.revisions; + this.slider = options.slider; + + this.listenTo( this.slider, 'hovered:revision', this.updateRevision ); + this.listenTo( this.slider, 'change:hovering', this.setHovering ); + this.listenTo( this.slider, 'change:scrubbing', this.setScrubbing ); + }, + + + updateRevision: function( revision ) { + this.set({ revision: revision }); + }, + + setHovering: function( model, value ) { + this.set({ hovering: value }); + }, + + setScrubbing: function( model, value ) { + this.set({ scrubbing: value }); + } + }); + + revisions.model.Revision = Backbone.Model.extend({}); + + /** + * wp.revisions.model.Revisions + * + * A collection of post revisions. + */ + revisions.model.Revisions = Backbone.Collection.extend({ + model: revisions.model.Revision, + + initialize: function() { + _.bindAll( this, 'next', 'prev' ); + }, + + next: function( revision ) { + var index = this.indexOf( revision ); + + if ( index !== -1 && index !== this.length - 1 ) { + return this.at( index + 1 ); + } + }, + + prev: function( revision ) { + var index = this.indexOf( revision ); + + if ( index !== -1 && index !== 0 ) { + return this.at( index - 1 ); + } + } + }); + + revisions.model.Field = Backbone.Model.extend({}); + + revisions.model.Fields = Backbone.Collection.extend({ + model: revisions.model.Field + }); + + revisions.model.Diff = Backbone.Model.extend({ + initialize: function() { + var fields = this.get('fields'); + this.unset('fields'); + + this.fields = new revisions.model.Fields( fields ); + } + }); + + revisions.model.Diffs = Backbone.Collection.extend({ + initialize: function( models, options ) { + _.bindAll( this, 'getClosestUnloaded' ); + this.loadAll = _.once( this._loadAll ); + this.revisions = options.revisions; + this.postId = options.postId; + this.requests = {}; + }, + + model: revisions.model.Diff, + + ensure: function( id, context ) { + var diff = this.get( id ), + request = this.requests[ id ], + deferred = $.Deferred(), + ids = {}, + from = id.split(':')[0], + to = id.split(':')[1]; + ids[id] = true; + + wp.revisions.log( 'ensure', id ); + + this.trigger( 'ensure', ids, from, to, deferred.promise() ); + + if ( diff ) { + deferred.resolveWith( context, [ diff ] ); + } else { + this.trigger( 'ensure:load', ids, from, to, deferred.promise() ); + _.each( ids, _.bind( function( id ) { + // Remove anything that has an ongoing request. + if ( this.requests[ id ] ) { + delete ids[ id ]; + } + // Remove anything we already have. + if ( this.get( id ) ) { + delete ids[ id ]; + } + }, this ) ); + if ( ! request ) { + // Always include the ID that started this ensure. + ids[ id ] = true; + request = this.load( _.keys( ids ) ); + } + + request.done( _.bind( function() { + deferred.resolveWith( context, [ this.get( id ) ] ); + }, this ) ).fail( _.bind( function() { + deferred.reject(); + }) ); + } + + return deferred.promise(); + }, + + // Returns an array of proximal diffs. + getClosestUnloaded: function( ids, centerId ) { + var self = this; + return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) { + return Math.abs( centerId - pair[1] ); + }).map( function( pair ) { + return pair.join(':'); + }).filter( function( diffId ) { + return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ]; + }).value(); + }, + + _loadAll: function( allRevisionIds, centerId, num ) { + var self = this, deferred = $.Deferred(), + diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num ); + if ( _.size( diffs ) > 0 ) { + this.load( diffs ).done( function() { + self._loadAll( allRevisionIds, centerId, num ).done( function() { + deferred.resolve(); + }); + }).fail( function() { + if ( 1 === num ) { // Already tried 1. This just isn't working. Give up. + deferred.reject(); + } else { // Request fewer diffs this time. + self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() { + deferred.resolve(); + }); + } + }); + } else { + deferred.resolve(); + } + return deferred; + }, + + load: function( comparisons ) { + wp.revisions.log( 'load', comparisons ); + // Our collection should only ever grow, never shrink, so `remove: false`. + return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() { + wp.revisions.log( 'load:complete', comparisons ); + }); + }, + + sync: function( method, model, options ) { + if ( 'read' === method ) { + options = options || {}; + options.context = this; + options.data = _.extend( options.data || {}, { + action: 'get-revision-diffs', + post_id: this.postId + }); + + var deferred = wp.ajax.send( options ), + requests = this.requests; + + // Record that we're requesting each diff. + if ( options.data.compare ) { + _.each( options.data.compare, function( id ) { + requests[ id ] = deferred; + }); + } + + // When the request completes, clear the stored request. + deferred.always( function() { + if ( options.data.compare ) { + _.each( options.data.compare, function( id ) { + delete requests[ id ]; + }); + } + }); + + return deferred; + + // Otherwise, fall back to `Backbone.sync()`. + } else { + return Backbone.Model.prototype.sync.apply( this, arguments ); + } + } + }); + + + /** + * wp.revisions.model.FrameState + * + * The frame state. + * + * @see wp.revisions.view.Frame + * + * @param {object} attributes Model attributes - none are required. + * @param {object} options Options for the model. + * @param {revisions.model.Revisions} options.revisions A collection of revisions. + */ + revisions.model.FrameState = Backbone.Model.extend({ + defaults: { + loading: false, + error: false, + compareTwoMode: false + }, + + initialize: function( attributes, options ) { + var state = this.get( 'initialDiffState' ); + _.bindAll( this, 'receiveDiff' ); + this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 ); + + this.revisions = options.revisions; + + this.diffs = new revisions.model.Diffs( [], { + revisions: this.revisions, + postId: this.get( 'postId' ) + } ); + + // Set the initial diffs collection. + this.diffs.set( this.get( 'diffData' ) ); + + // Set up internal listeners. + this.listenTo( this, 'change:from', this.changeRevisionHandler ); + this.listenTo( this, 'change:to', this.changeRevisionHandler ); + this.listenTo( this, 'change:compareTwoMode', this.changeMode ); + this.listenTo( this, 'update:revisions', this.updatedRevisions ); + this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus ); + this.listenTo( this, 'update:diff', this.updateLoadingStatus ); + + // Set the initial revisions, baseUrl, and mode as provided through attributes. + + this.set( { + to : this.revisions.get( state.to ), + from : this.revisions.get( state.from ), + compareTwoMode : state.compareTwoMode + } ); + + // Start the router if browser supports History API. + if ( window.history && window.history.pushState ) { + this.router = new revisions.Router({ model: this }); + if ( Backbone.History.started ) { + Backbone.history.stop(); + } + Backbone.history.start({ pushState: true }); + } + }, + + updateLoadingStatus: function() { + this.set( 'error', false ); + this.set( 'loading', ! this.diff() ); + }, + + changeMode: function( model, value ) { + var toIndex = this.revisions.indexOf( this.get( 'to' ) ); + + // If we were on the first revision before switching to two-handled mode, + // bump the 'to' position over one. + if ( value && 0 === toIndex ) { + this.set({ + from: this.revisions.at( toIndex ), + to: this.revisions.at( toIndex + 1 ) + }); + } + + // When switching back to single-handled mode, reset 'from' model to + // one position before the 'to' model. + if ( ! value && 0 !== toIndex ) { // '! value' means switching to single-handled mode. + this.set({ + from: this.revisions.at( toIndex - 1 ), + to: this.revisions.at( toIndex ) + }); + } + }, + + updatedRevisions: function( from, to ) { + if ( this.get( 'compareTwoMode' ) ) { + // @todo Compare-two loading strategy. + } else { + this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 ); + } + }, + + // Fetch the currently loaded diff. + diff: function() { + return this.diffs.get( this._diffId ); + }, + + /* + * So long as `from` and `to` are changed at the same time, the diff + * will only be updated once. This is because Backbone updates all of + * the changed attributes in `set`, and then fires the `change` events. + */ + updateDiff: function( options ) { + var from, to, diffId, diff; + + options = options || {}; + from = this.get('from'); + to = this.get('to'); + diffId = ( from ? from.id : 0 ) + ':' + to.id; + + // Check if we're actually changing the diff id. + if ( this._diffId === diffId ) { + return $.Deferred().reject().promise(); + } + + this._diffId = diffId; + this.trigger( 'update:revisions', from, to ); + + diff = this.diffs.get( diffId ); + + // If we already have the diff, then immediately trigger the update. + if ( diff ) { + this.receiveDiff( diff ); + return $.Deferred().resolve().promise(); + // Otherwise, fetch the diff. + } else { + if ( options.immediate ) { + return this._ensureDiff(); + } else { + this._debouncedEnsureDiff(); + return $.Deferred().reject().promise(); + } + } + }, + + // A simple wrapper around `updateDiff` to prevent the change event's + // parameters from being passed through. + changeRevisionHandler: function() { + this.updateDiff(); + }, + + receiveDiff: function( diff ) { + // Did we actually get a diff? + if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) { + this.set({ + loading: false, + error: true + }); + } else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change. + this.trigger( 'update:diff', diff ); + } + }, + + _ensureDiff: function() { + return this.diffs.ensure( this._diffId, this ).always( this.receiveDiff ); + } + }); + + + /** + * ======================================================================== + * VIEWS + * ======================================================================== + */ + + /** + * wp.revisions.view.Frame + * + * Top level frame that orchestrates the revisions experience. + * + * @param {object} options The options hash for the view. + * @param {revisions.model.FrameState} options.model The frame state model. + */ + revisions.view.Frame = wp.Backbone.View.extend({ + className: 'revisions', + template: wp.template('revisions-frame'), + + initialize: function() { + this.listenTo( this.model, 'update:diff', this.renderDiff ); + this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode ); + this.listenTo( this.model, 'change:loading', this.updateLoadingStatus ); + this.listenTo( this.model, 'change:error', this.updateErrorStatus ); + + this.views.set( '.revisions-control-frame', new revisions.view.Controls({ + model: this.model + }) ); + }, + + render: function() { + wp.Backbone.View.prototype.render.apply( this, arguments ); + + $('html').css( 'overflow-y', 'scroll' ); + $('#wpbody-content .wrap').append( this.el ); + this.updateCompareTwoMode(); + this.renderDiff( this.model.diff() ); + this.views.ready(); + + return this; + }, + + renderDiff: function( diff ) { + this.views.set( '.revisions-diff-frame', new revisions.view.Diff({ + model: diff + }) ); + }, + + updateLoadingStatus: function() { + this.$el.toggleClass( 'loading', this.model.get('loading') ); + }, + + updateErrorStatus: function() { + this.$el.toggleClass( 'diff-error', this.model.get('error') ); + }, + + updateCompareTwoMode: function() { + this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') ); + } + }); + + /** + * wp.revisions.view.Controls + * + * The controls view. + * + * Contains the revision slider, previous/next buttons, the meta info and the compare checkbox. + */ + revisions.view.Controls = wp.Backbone.View.extend({ + className: 'revisions-controls', + + initialize: function() { + _.bindAll( this, 'setWidth' ); + + // Add the button view. + this.views.add( new revisions.view.Buttons({ + model: this.model + }) ); + + // Add the checkbox view. + this.views.add( new revisions.view.Checkbox({ + model: this.model + }) ); + + // Prep the slider model. + var slider = new revisions.model.Slider({ + frame: this.model, + revisions: this.model.revisions + }), + + // Prep the tooltip model. + tooltip = new revisions.model.Tooltip({ + frame: this.model, + revisions: this.model.revisions, + slider: slider + }); + + // Add the tooltip view. + this.views.add( new revisions.view.Tooltip({ + model: tooltip + }) ); + + // Add the tickmarks view. + this.views.add( new revisions.view.Tickmarks({ + model: tooltip + }) ); + + // Add the slider view. + this.views.add( new revisions.view.Slider({ + model: slider + }) ); + + // Add the Metabox view. + this.views.add( new revisions.view.Metabox({ + model: this.model + }) ); + }, + + ready: function() { + this.top = this.$el.offset().top; + this.window = $(window); + this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) { + var controls = e.data.controls, + container = controls.$el.parent(), + scrolled = controls.window.scrollTop(), + frame = controls.views.parent; + + if ( scrolled >= controls.top ) { + if ( ! frame.$el.hasClass('pinned') ) { + controls.setWidth(); + container.css('height', container.height() + 'px' ); + controls.window.on('resize.wp.revisions.pinning click.wp.revisions.pinning', {controls: controls}, function(e) { + e.data.controls.setWidth(); + }); + } + frame.$el.addClass('pinned'); + } else if ( frame.$el.hasClass('pinned') ) { + controls.window.off('.wp.revisions.pinning'); + controls.$el.css('width', 'auto'); + frame.$el.removeClass('pinned'); + container.css('height', 'auto'); + controls.top = controls.$el.offset().top; + } else { + controls.top = controls.$el.offset().top; + } + }); + }, + + setWidth: function() { + this.$el.css('width', this.$el.parent().width() + 'px'); + } + }); + + // The tickmarks view. + revisions.view.Tickmarks = wp.Backbone.View.extend({ + className: 'revisions-tickmarks', + direction: isRtl ? 'right' : 'left', + + initialize: function() { + this.listenTo( this.model, 'change:revision', this.reportTickPosition ); + }, + + reportTickPosition: function( model, revision ) { + var offset, thisOffset, parentOffset, tick, index = this.model.revisions.indexOf( revision ); + thisOffset = this.$el.allOffsets(); + parentOffset = this.$el.parent().allOffsets(); + if ( index === this.model.revisions.length - 1 ) { + // Last one. + offset = { + rightPlusWidth: thisOffset.left - parentOffset.left + 1, + leftPlusWidth: thisOffset.right - parentOffset.right + 1 + }; + } else { + // Normal tick. + tick = this.$('div:nth-of-type(' + (index + 1) + ')'); + offset = tick.allPositions(); + _.extend( offset, { + left: offset.left + thisOffset.left - parentOffset.left, + right: offset.right + thisOffset.right - parentOffset.right + }); + _.extend( offset, { + leftPlusWidth: offset.left + tick.outerWidth(), + rightPlusWidth: offset.right + tick.outerWidth() + }); + } + this.model.set({ offset: offset }); + }, + + ready: function() { + var tickCount, tickWidth; + tickCount = this.model.revisions.length - 1; + tickWidth = 1 / tickCount; + this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px'); + + _(tickCount).times( function( index ){ + this.$el.append( '<div style="' + this.direction + ': ' + ( 100 * tickWidth * index ) + '%"></div>' ); + }, this ); + } + }); + + // The metabox view. + revisions.view.Metabox = wp.Backbone.View.extend({ + className: 'revisions-meta', + + initialize: function() { + // Add the 'from' view. + this.views.add( new revisions.view.MetaFrom({ + model: this.model, + className: 'diff-meta diff-meta-from' + }) ); + + // Add the 'to' view. + this.views.add( new revisions.view.MetaTo({ + model: this.model + }) ); + } + }); + + // The revision meta view (to be extended). + revisions.view.Meta = wp.Backbone.View.extend({ + template: wp.template('revisions-meta'), + + events: { + 'click .restore-revision': 'restoreRevision' + }, + + initialize: function() { + this.listenTo( this.model, 'update:revisions', this.render ); + }, + + prepare: function() { + return _.extend( this.model.toJSON()[this.type] || {}, { + type: this.type + }); + }, + + restoreRevision: function() { + document.location = this.model.get('to').attributes.restoreUrl; + } + }); + + // The revision meta 'from' view. + revisions.view.MetaFrom = revisions.view.Meta.extend({ + className: 'diff-meta diff-meta-from', + type: 'from' + }); + + // The revision meta 'to' view. + revisions.view.MetaTo = revisions.view.Meta.extend({ + className: 'diff-meta diff-meta-to', + type: 'to' + }); + + // The checkbox view. + revisions.view.Checkbox = wp.Backbone.View.extend({ + className: 'revisions-checkbox', + template: wp.template('revisions-checkbox'), + + events: { + 'click .compare-two-revisions': 'compareTwoToggle' + }, + + initialize: function() { + this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode ); + }, + + ready: function() { + if ( this.model.revisions.length < 3 ) { + $('.revision-toggle-compare-mode').hide(); + } + }, + + updateCompareTwoMode: function() { + this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') ); + }, + + // Toggle the compare two mode feature when the compare two checkbox is checked. + compareTwoToggle: function() { + // Activate compare two mode? + this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') }); + } + }); + + // The tooltip view. + // Encapsulates the tooltip. + revisions.view.Tooltip = wp.Backbone.View.extend({ + className: 'revisions-tooltip', + template: wp.template('revisions-meta'), + + initialize: function() { + this.listenTo( this.model, 'change:offset', this.render ); + this.listenTo( this.model, 'change:hovering', this.toggleVisibility ); + this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility ); + }, + + prepare: function() { + if ( _.isNull( this.model.get('revision') ) ) { + return; + } else { + return _.extend( { type: 'tooltip' }, { + attributes: this.model.get('revision').toJSON() + }); + } + }, + + render: function() { + var otherDirection, + direction, + directionVal, + flipped, + css = {}, + position = this.model.revisions.indexOf( this.model.get('revision') ) + 1; + + flipped = ( position / this.model.revisions.length ) > 0.5; + if ( isRtl ) { + direction = flipped ? 'left' : 'right'; + directionVal = flipped ? 'leftPlusWidth' : direction; + } else { + direction = flipped ? 'right' : 'left'; + directionVal = flipped ? 'rightPlusWidth' : direction; + } + otherDirection = 'right' === direction ? 'left': 'right'; + wp.Backbone.View.prototype.render.apply( this, arguments ); + css[direction] = this.model.get('offset')[directionVal] + 'px'; + css[otherDirection] = ''; + this.$el.toggleClass( 'flipped', flipped ).css( css ); + }, + + visible: function() { + return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' ); + }, + + toggleVisibility: function() { + if ( this.visible() ) { + this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 ); + } else { + this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } ); + } + return; + } + }); + + // The buttons view. + // Encapsulates all of the configuration for the previous/next buttons. + revisions.view.Buttons = wp.Backbone.View.extend({ + className: 'revisions-buttons', + template: wp.template('revisions-buttons'), + + events: { + 'click .revisions-next .button': 'nextRevision', + 'click .revisions-previous .button': 'previousRevision' + }, + + initialize: function() { + this.listenTo( this.model, 'update:revisions', this.disabledButtonCheck ); + }, + + ready: function() { + this.disabledButtonCheck(); + }, + + // Go to a specific model index. + gotoModel: function( toIndex ) { + var attributes = { + to: this.model.revisions.at( toIndex ) + }; + // If we're at the first revision, unset 'from'. + if ( toIndex ) { + attributes.from = this.model.revisions.at( toIndex - 1 ); + } else { + this.model.unset('from', { silent: true }); + } + + this.model.set( attributes ); + }, + + // Go to the 'next' revision. + nextRevision: function() { + var toIndex = this.model.revisions.indexOf( this.model.get('to') ) + 1; + this.gotoModel( toIndex ); + }, + + // Go to the 'previous' revision. + previousRevision: function() { + var toIndex = this.model.revisions.indexOf( this.model.get('to') ) - 1; + this.gotoModel( toIndex ); + }, + + // Check to see if the Previous or Next buttons need to be disabled or enabled. + disabledButtonCheck: function() { + var maxVal = this.model.revisions.length - 1, + minVal = 0, + next = $('.revisions-next .button'), + previous = $('.revisions-previous .button'), + val = this.model.revisions.indexOf( this.model.get('to') ); + + // Disable "Next" button if you're on the last node. + next.prop( 'disabled', ( maxVal === val ) ); + + // Disable "Previous" button if you're on the first node. + previous.prop( 'disabled', ( minVal === val ) ); + } + }); + + + // The slider view. + revisions.view.Slider = wp.Backbone.View.extend({ + className: 'wp-slider', + direction: isRtl ? 'right' : 'left', + + events: { + 'mousemove' : 'mouseMove' + }, + + initialize: function() { + _.bindAll( this, 'start', 'slide', 'stop', 'mouseMove', 'mouseEnter', 'mouseLeave' ); + this.listenTo( this.model, 'update:slider', this.applySliderSettings ); + }, + + ready: function() { + this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px'); + this.$el.slider( _.extend( this.model.toJSON(), { + start: this.start, + slide: this.slide, + stop: this.stop + }) ); + + this.$el.hoverIntent({ + over: this.mouseEnter, + out: this.mouseLeave, + timeout: 800 + }); + + this.applySliderSettings(); + }, + + mouseMove: function( e ) { + var zoneCount = this.model.revisions.length - 1, // One fewer zone than models. + sliderFrom = this.$el.allOffsets()[this.direction], // "From" edge of slider. + sliderWidth = this.$el.width(), // Width of slider. + tickWidth = sliderWidth / zoneCount, // Calculated width of zone. + actualX = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom. + currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 ) ) / tickWidth ); // Calculate the model index. + + // Ensure sane value for currentModelIndex. + if ( currentModelIndex < 0 ) { + currentModelIndex = 0; + } else if ( currentModelIndex >= this.model.revisions.length ) { + currentModelIndex = this.model.revisions.length - 1; + } + + // Update the tooltip mode. + this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) }); + }, + + mouseLeave: function() { + this.model.set({ hovering: false }); + }, + + mouseEnter: function() { + this.model.set({ hovering: true }); + }, + + applySliderSettings: function() { + this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) ); + var handles = this.$('a.ui-slider-handle'); + + if ( this.model.get('compareTwoMode') ) { + // In RTL mode the 'left handle' is the second in the slider, 'right' is first. + handles.first() + .toggleClass( 'to-handle', !! isRtl ) + .toggleClass( 'from-handle', ! isRtl ); + handles.last() + .toggleClass( 'from-handle', !! isRtl ) + .toggleClass( 'to-handle', ! isRtl ); + } else { + handles.removeClass('from-handle to-handle'); + } + }, + + start: function( event, ui ) { + this.model.set({ scrubbing: true }); + + // Track the mouse position to enable smooth dragging, + // overrides default jQuery UI step behavior. + $( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) { + var handles, + view = e.data.view, + leftDragBoundary = view.$el.offset().left, + sliderOffset = leftDragBoundary, + sliderRightEdge = leftDragBoundary + view.$el.width(), + rightDragBoundary = sliderRightEdge, + leftDragReset = '0', + rightDragReset = '100%', + handle = $( ui.handle ); + + // In two handle mode, ensure handles can't be dragged past each other. + // Adjust left/right boundaries and reset points. + if ( view.model.get('compareTwoMode') ) { + handles = handle.parent().find('.ui-slider-handle'); + if ( handle.is( handles.first() ) ) { + // We're the left handle. + rightDragBoundary = handles.last().offset().left; + rightDragReset = rightDragBoundary - sliderOffset; + } else { + // We're the right handle. + leftDragBoundary = handles.first().offset().left + handles.first().width(); + leftDragReset = leftDragBoundary - sliderOffset; + } + } + + // Follow mouse movements, as long as handle remains inside slider. + if ( e.pageX < leftDragBoundary ) { + handle.css( 'left', leftDragReset ); // Mouse to left of slider. + } else if ( e.pageX > rightDragBoundary ) { + handle.css( 'left', rightDragReset ); // Mouse to right of slider. + } else { + handle.css( 'left', e.pageX - sliderOffset ); // Mouse in slider. + } + } ); + }, + + getPosition: function( position ) { + return isRtl ? this.model.revisions.length - position - 1: position; + }, + + // Responds to slide events. + slide: function( event, ui ) { + var attributes, movedRevision; + // Compare two revisions mode. + if ( this.model.get('compareTwoMode') ) { + // Prevent sliders from occupying same spot. + if ( ui.values[1] === ui.values[0] ) { + return false; + } + if ( isRtl ) { + ui.values.reverse(); + } + attributes = { + from: this.model.revisions.at( this.getPosition( ui.values[0] ) ), + to: this.model.revisions.at( this.getPosition( ui.values[1] ) ) + }; + } else { + attributes = { + to: this.model.revisions.at( this.getPosition( ui.value ) ) + }; + // If we're at the first revision, unset 'from'. + if ( this.getPosition( ui.value ) > 0 ) { + attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 ); + } else { + attributes.from = undefined; + } + } + movedRevision = this.model.revisions.at( this.getPosition( ui.value ) ); + + // If we are scrubbing, a scrub to a revision is considered a hover. + if ( this.model.get('scrubbing') ) { + attributes.hoveredRevision = movedRevision; + } + + this.model.set( attributes ); + }, + + stop: function() { + $( window ).off('mousemove.wp.revisions'); + this.model.updateSliderSettings(); // To snap us back to a tick mark. + this.model.set({ scrubbing: false }); + } + }); + + // The diff view. + // This is the view for the current active diff. + revisions.view.Diff = wp.Backbone.View.extend({ + className: 'revisions-diff', + template: wp.template('revisions-diff'), + + // Generate the options to be passed to the template. + prepare: function() { + return _.extend({ fields: this.model.fields.toJSON() }, this.options ); + } + }); + + // The revisions router. + // Maintains the URL routes so browser URL matches state. + revisions.Router = Backbone.Router.extend({ + initialize: function( options ) { + this.model = options.model; + + // Maintain state and history when navigating. + this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) ); + this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl ); + }, + + baseUrl: function( url ) { + return this.model.get('baseUrl') + url; + }, + + updateUrl: function() { + var from = this.model.has('from') ? this.model.get('from').id : 0, + to = this.model.get('to').id; + if ( this.model.get('compareTwoMode' ) ) { + this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ), { replace: true } ); + } else { + this.navigate( this.baseUrl( '?revision=' + to ), { replace: true } ); + } + }, + + handleRoute: function( a, b ) { + var compareTwo = _.isUndefined( b ); + + if ( ! compareTwo ) { + b = this.model.revisions.get( a ); + a = this.model.revisions.prev( b ); + b = b ? b.id : 0; + a = a ? a.id : 0; + } + } + }); + + /** + * Initialize the revisions UI for revision.php. + */ + revisions.init = function() { + var state; + + // Bail if the current page is not revision.php. + if ( ! window.adminpage || 'revision-php' !== window.adminpage ) { + return; + } + + state = new revisions.model.FrameState({ + initialDiffState: { + // wp_localize_script doesn't stringifies ints, so cast them. + to: parseInt( revisions.settings.to, 10 ), + from: parseInt( revisions.settings.from, 10 ), + // wp_localize_script does not allow for top-level booleans so do a comparator here. + compareTwoMode: ( revisions.settings.compareTwoMode === '1' ) + }, + diffData: revisions.settings.diffData, + baseUrl: revisions.settings.baseUrl, + postId: parseInt( revisions.settings.postId, 10 ) + }, { + revisions: new revisions.model.Revisions( revisions.settings.revisionData ) + }); + + revisions.view.frame = new revisions.view.Frame({ + model: state + }).render(); + }; + + $( revisions.init ); +}(jQuery)); diff --git a/wp-admin/js/revisions.min.js b/wp-admin/js/revisions.min.js new file mode 100644 index 0000000..95ae999 --- /dev/null +++ b/wp-admin/js/revisions.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},function(a){var s=wp.revisions={model:{},view:{},controller:{}};s.settings=window._wpRevisionsSettings||{},s.debug=!1,s.log=function(){window.console&&s.debug&&window.console.log.apply(window.console,arguments)},a.fn.allOffsets=function(){var e=this.offset()||{top:0,left:0},i=a(window);return _.extend(e,{right:i.width()-e.left-this.outerWidth(),bottom:i.height()-e.top-this.outerHeight()})},a.fn.allPositions=function(){var e=this.position()||{top:0,left:0},i=this.parent();return _.extend(e,{right:i.outerWidth()-e.left-this.outerWidth(),bottom:i.outerHeight()-e.top-this.outerHeight()})},s.model.Slider=Backbone.Model.extend({defaults:{value:null,values:null,min:0,max:1,step:1,range:!1,compareTwoMode:!1},initialize:function(e){this.frame=e.frame,this.revisions=e.revisions,this.listenTo(this.frame,"update:revisions",this.receiveRevisions),this.listenTo(this.frame,"change:compareTwoMode",this.updateMode),this.on("change:from",this.handleLocalChanges),this.on("change:to",this.handleLocalChanges),this.on("change:compareTwoMode",this.updateSliderSettings),this.on("update:revisions",this.updateSliderSettings),this.on("change:hoveredRevision",this.hoverRevision),this.set({max:this.revisions.length-1,compareTwoMode:this.frame.get("compareTwoMode"),from:this.frame.get("from"),to:this.frame.get("to")}),this.updateSliderSettings()},getSliderValue:function(e,i){return isRtl?this.revisions.length-this.revisions.indexOf(this.get(e))-1:this.revisions.indexOf(this.get(i))},updateSliderSettings:function(){this.get("compareTwoMode")?this.set({values:[this.getSliderValue("to","from"),this.getSliderValue("from","to")],value:null,range:!0}):this.set({value:this.getSliderValue("to","to"),values:null,range:!1}),this.trigger("update:slider")},hoverRevision:function(e,i){this.trigger("hovered:revision",i)},updateMode:function(e,i){this.set({compareTwoMode:i})},handleLocalChanges:function(){this.frame.set({from:this.get("from"),to:this.get("to")})},receiveRevisions:function(e,i){this.get("from")===e&&this.get("to")===i||(this.set({from:e,to:i},{silent:!0}),this.trigger("update:revisions",e,i))}}),s.model.Tooltip=Backbone.Model.extend({defaults:{revision:null,offset:{},hovering:!1,scrubbing:!1},initialize:function(e){this.frame=e.frame,this.revisions=e.revisions,this.slider=e.slider,this.listenTo(this.slider,"hovered:revision",this.updateRevision),this.listenTo(this.slider,"change:hovering",this.setHovering),this.listenTo(this.slider,"change:scrubbing",this.setScrubbing)},updateRevision:function(e){this.set({revision:e})},setHovering:function(e,i){this.set({hovering:i})},setScrubbing:function(e,i){this.set({scrubbing:i})}}),s.model.Revision=Backbone.Model.extend({}),s.model.Revisions=Backbone.Collection.extend({model:s.model.Revision,initialize:function(){_.bindAll(this,"next","prev")},next:function(e){e=this.indexOf(e);if(-1!==e&&e!==this.length-1)return this.at(e+1)},prev:function(e){e=this.indexOf(e);if(-1!==e&&0!==e)return this.at(e-1)}}),s.model.Field=Backbone.Model.extend({}),s.model.Fields=Backbone.Collection.extend({model:s.model.Field}),s.model.Diff=Backbone.Model.extend({initialize:function(){var e=this.get("fields");this.unset("fields"),this.fields=new s.model.Fields(e)}}),s.model.Diffs=Backbone.Collection.extend({initialize:function(e,i){_.bindAll(this,"getClosestUnloaded"),this.loadAll=_.once(this._loadAll),this.revisions=i.revisions,this.postId=i.postId,this.requests={}},model:s.model.Diff,ensure:function(e,i){var t=this.get(e),s=this.requests[e],o=a.Deferred(),n={},r=e.split(":")[0],l=e.split(":")[1];return n[e]=!0,wp.revisions.log("ensure",e),this.trigger("ensure",n,r,l,o.promise()),t?o.resolveWith(i,[t]):(this.trigger("ensure:load",n,r,l,o.promise()),_.each(n,_.bind(function(e){this.requests[e]&&delete n[e],this.get(e)&&delete n[e]},this)),s||(n[e]=!0,s=this.load(_.keys(n))),s.done(_.bind(function(){o.resolveWith(i,[this.get(e)])},this)).fail(_.bind(function(){o.reject()}))),o.promise()},getClosestUnloaded:function(e,i){var t=this;return _.chain([0].concat(e)).initial().zip(e).sortBy(function(e){return Math.abs(i-e[1])}).map(function(e){return e.join(":")}).filter(function(e){return _.isUndefined(t.get(e))&&!t.requests[e]}).value()},_loadAll:function(e,i,t){var s=this,o=a.Deferred(),n=_.first(this.getClosestUnloaded(e,i),t);return 0<_.size(n)?this.load(n).done(function(){s._loadAll(e,i,t).done(function(){o.resolve()})}).fail(function(){1===t?o.reject():s._loadAll(e,i,Math.ceil(t/2)).done(function(){o.resolve()})}):o.resolve(),o},load:function(e){return wp.revisions.log("load",e),this.fetch({data:{compare:e},remove:!1}).done(function(){wp.revisions.log("load:complete",e)})},sync:function(e,i,t){var s,o;return"read"===e?((t=t||{}).context=this,t.data=_.extend(t.data||{},{action:"get-revision-diffs",post_id:this.postId}),s=wp.ajax.send(t),o=this.requests,t.data.compare&&_.each(t.data.compare,function(e){o[e]=s}),s.always(function(){t.data.compare&&_.each(t.data.compare,function(e){delete o[e]})}),s):Backbone.Model.prototype.sync.apply(this,arguments)}}),s.model.FrameState=Backbone.Model.extend({defaults:{loading:!1,error:!1,compareTwoMode:!1},initialize:function(e,i){var t=this.get("initialDiffState");_.bindAll(this,"receiveDiff"),this._debouncedEnsureDiff=_.debounce(this._ensureDiff,200),this.revisions=i.revisions,this.diffs=new s.model.Diffs([],{revisions:this.revisions,postId:this.get("postId")}),this.diffs.set(this.get("diffData")),this.listenTo(this,"change:from",this.changeRevisionHandler),this.listenTo(this,"change:to",this.changeRevisionHandler),this.listenTo(this,"change:compareTwoMode",this.changeMode),this.listenTo(this,"update:revisions",this.updatedRevisions),this.listenTo(this.diffs,"ensure:load",this.updateLoadingStatus),this.listenTo(this,"update:diff",this.updateLoadingStatus),this.set({to:this.revisions.get(t.to),from:this.revisions.get(t.from),compareTwoMode:t.compareTwoMode}),window.history&&window.history.pushState&&(this.router=new s.Router({model:this}),Backbone.History.started&&Backbone.history.stop(),Backbone.history.start({pushState:!0}))},updateLoadingStatus:function(){this.set("error",!1),this.set("loading",!this.diff())},changeMode:function(e,i){var t=this.revisions.indexOf(this.get("to"));i&&0===t&&this.set({from:this.revisions.at(t),to:this.revisions.at(t+1)}),i||0===t||this.set({from:this.revisions.at(t-1),to:this.revisions.at(t)})},updatedRevisions:function(e,i){this.get("compareTwoMode")||this.diffs.loadAll(this.revisions.pluck("id"),i.id,40)},diff:function(){return this.diffs.get(this._diffId)},updateDiff:function(e){var i,t,s;return e=e||{},s=this.get("from"),i=this.get("to"),t=(s?s.id:0)+":"+i.id,this._diffId===t?a.Deferred().reject().promise():(this._diffId=t,this.trigger("update:revisions",s,i),(s=this.diffs.get(t))?(this.receiveDiff(s),a.Deferred().resolve().promise()):e.immediate?this._ensureDiff():(this._debouncedEnsureDiff(),a.Deferred().reject().promise()))},changeRevisionHandler:function(){this.updateDiff()},receiveDiff:function(e){_.isUndefined(e)||_.isUndefined(e.id)?this.set({loading:!1,error:!0}):this._diffId===e.id&&this.trigger("update:diff",e)},_ensureDiff:function(){return this.diffs.ensure(this._diffId,this).always(this.receiveDiff)}}),s.view.Frame=wp.Backbone.View.extend({className:"revisions",template:wp.template("revisions-frame"),initialize:function(){this.listenTo(this.model,"update:diff",this.renderDiff),this.listenTo(this.model,"change:compareTwoMode",this.updateCompareTwoMode),this.listenTo(this.model,"change:loading",this.updateLoadingStatus),this.listenTo(this.model,"change:error",this.updateErrorStatus),this.views.set(".revisions-control-frame",new s.view.Controls({model:this.model}))},render:function(){return wp.Backbone.View.prototype.render.apply(this,arguments),a("html").css("overflow-y","scroll"),a("#wpbody-content .wrap").append(this.el),this.updateCompareTwoMode(),this.renderDiff(this.model.diff()),this.views.ready(),this},renderDiff:function(e){this.views.set(".revisions-diff-frame",new s.view.Diff({model:e}))},updateLoadingStatus:function(){this.$el.toggleClass("loading",this.model.get("loading"))},updateErrorStatus:function(){this.$el.toggleClass("diff-error",this.model.get("error"))},updateCompareTwoMode:function(){this.$el.toggleClass("comparing-two-revisions",this.model.get("compareTwoMode"))}}),s.view.Controls=wp.Backbone.View.extend({className:"revisions-controls",initialize:function(){_.bindAll(this,"setWidth"),this.views.add(new s.view.Buttons({model:this.model})),this.views.add(new s.view.Checkbox({model:this.model}));var e=new s.model.Slider({frame:this.model,revisions:this.model.revisions}),i=new s.model.Tooltip({frame:this.model,revisions:this.model.revisions,slider:e});this.views.add(new s.view.Tooltip({model:i})),this.views.add(new s.view.Tickmarks({model:i})),this.views.add(new s.view.Slider({model:e})),this.views.add(new s.view.Metabox({model:this.model}))},ready:function(){this.top=this.$el.offset().top,this.window=a(window),this.window.on("scroll.wp.revisions",{controls:this},function(e){var e=e.data.controls,i=e.$el.parent(),t=e.window.scrollTop(),s=e.views.parent;t>=e.top?(s.$el.hasClass("pinned")||(e.setWidth(),i.css("height",i.height()+"px"),e.window.on("resize.wp.revisions.pinning click.wp.revisions.pinning",{controls:e},function(e){e.data.controls.setWidth()})),s.$el.addClass("pinned")):(s.$el.hasClass("pinned")&&(e.window.off(".wp.revisions.pinning"),e.$el.css("width","auto"),s.$el.removeClass("pinned"),i.css("height","auto")),e.top=e.$el.offset().top)})},setWidth:function(){this.$el.css("width",this.$el.parent().width()+"px")}}),s.view.Tickmarks=wp.Backbone.View.extend({className:"revisions-tickmarks",direction:isRtl?"right":"left",initialize:function(){this.listenTo(this.model,"change:revision",this.reportTickPosition)},reportTickPosition:function(e,i){var t,i=this.model.revisions.indexOf(i),s=this.$el.allOffsets(),o=this.$el.parent().allOffsets();i===this.model.revisions.length-1?t={rightPlusWidth:s.left-o.left+1,leftPlusWidth:s.right-o.right+1}:(t=(i=this.$("div:nth-of-type("+(i+1)+")")).allPositions(),_.extend(t,{left:t.left+s.left-o.left,right:t.right+s.right-o.right}),_.extend(t,{leftPlusWidth:t.left+i.outerWidth(),rightPlusWidth:t.right+i.outerWidth()})),this.model.set({offset:t})},ready:function(){var e=this.model.revisions.length-1,i=1/e;this.$el.css("width",50*this.model.revisions.length+"px"),_(e).times(function(e){this.$el.append('<div style="'+this.direction+": "+100*i*e+'%"></div>')},this)}}),s.view.Metabox=wp.Backbone.View.extend({className:"revisions-meta",initialize:function(){this.views.add(new s.view.MetaFrom({model:this.model,className:"diff-meta diff-meta-from"})),this.views.add(new s.view.MetaTo({model:this.model}))}}),s.view.Meta=wp.Backbone.View.extend({template:wp.template("revisions-meta"),events:{"click .restore-revision":"restoreRevision"},initialize:function(){this.listenTo(this.model,"update:revisions",this.render)},prepare:function(){return _.extend(this.model.toJSON()[this.type]||{},{type:this.type})},restoreRevision:function(){document.location=this.model.get("to").attributes.restoreUrl}}),s.view.MetaFrom=s.view.Meta.extend({className:"diff-meta diff-meta-from",type:"from"}),s.view.MetaTo=s.view.Meta.extend({className:"diff-meta diff-meta-to",type:"to"}),s.view.Checkbox=wp.Backbone.View.extend({className:"revisions-checkbox",template:wp.template("revisions-checkbox"),events:{"click .compare-two-revisions":"compareTwoToggle"},initialize:function(){this.listenTo(this.model,"change:compareTwoMode",this.updateCompareTwoMode)},ready:function(){this.model.revisions.length<3&&a(".revision-toggle-compare-mode").hide()},updateCompareTwoMode:function(){this.$(".compare-two-revisions").prop("checked",this.model.get("compareTwoMode"))},compareTwoToggle:function(){this.model.set({compareTwoMode:a(".compare-two-revisions").prop("checked")})}}),s.view.Tooltip=wp.Backbone.View.extend({className:"revisions-tooltip",template:wp.template("revisions-meta"),initialize:function(){this.listenTo(this.model,"change:offset",this.render),this.listenTo(this.model,"change:hovering",this.toggleVisibility),this.listenTo(this.model,"change:scrubbing",this.toggleVisibility)},prepare:function(){if(!_.isNull(this.model.get("revision")))return _.extend({type:"tooltip"},{attributes:this.model.get("revision").toJSON()})},render:function(){var e,i={},t=.5<(this.model.revisions.indexOf(this.model.get("revision"))+1)/this.model.revisions.length,s=isRtl?(e=t?"left":"right",t?"leftPlusWidth":e):(e=t?"right":"left",t?"rightPlusWidth":e),o="right"===e?"left":"right";wp.Backbone.View.prototype.render.apply(this,arguments),i[e]=this.model.get("offset")[s]+"px",i[o]="",this.$el.toggleClass("flipped",t).css(i)},visible:function(){return this.model.get("scrubbing")||this.model.get("hovering")},toggleVisibility:function(){this.visible()?this.$el.stop().show().fadeTo(100-100*this.el.style.opacity,1):this.$el.stop().fadeTo(300*this.el.style.opacity,0,function(){a(this).hide()})}}),s.view.Buttons=wp.Backbone.View.extend({className:"revisions-buttons",template:wp.template("revisions-buttons"),events:{"click .revisions-next .button":"nextRevision","click .revisions-previous .button":"previousRevision"},initialize:function(){this.listenTo(this.model,"update:revisions",this.disabledButtonCheck)},ready:function(){this.disabledButtonCheck()},gotoModel:function(e){var i={to:this.model.revisions.at(e)};e?i.from=this.model.revisions.at(e-1):this.model.unset("from",{silent:!0}),this.model.set(i)},nextRevision:function(){var e=this.model.revisions.indexOf(this.model.get("to"))+1;this.gotoModel(e)},previousRevision:function(){var e=this.model.revisions.indexOf(this.model.get("to"))-1;this.gotoModel(e)},disabledButtonCheck:function(){var e=this.model.revisions.length-1,i=a(".revisions-next .button"),t=a(".revisions-previous .button"),s=this.model.revisions.indexOf(this.model.get("to"));i.prop("disabled",e===s),t.prop("disabled",0===s)}}),s.view.Slider=wp.Backbone.View.extend({className:"wp-slider",direction:isRtl?"right":"left",events:{mousemove:"mouseMove"},initialize:function(){_.bindAll(this,"start","slide","stop","mouseMove","mouseEnter","mouseLeave"),this.listenTo(this.model,"update:slider",this.applySliderSettings)},ready:function(){this.$el.css("width",50*this.model.revisions.length+"px"),this.$el.slider(_.extend(this.model.toJSON(),{start:this.start,slide:this.slide,stop:this.stop})),this.$el.hoverIntent({over:this.mouseEnter,out:this.mouseLeave,timeout:800}),this.applySliderSettings()},mouseMove:function(e){var i=this.model.revisions.length-1,t=this.$el.allOffsets()[this.direction],i=this.$el.width()/i,e=(isRtl?a(window).width()-e.pageX:e.pageX)-t,t=Math.floor((e+i/2)/i);t<0?t=0:t>=this.model.revisions.length&&(t=this.model.revisions.length-1),this.model.set({hoveredRevision:this.model.revisions.at(t)})},mouseLeave:function(){this.model.set({hovering:!1})},mouseEnter:function(){this.model.set({hovering:!0})},applySliderSettings:function(){this.$el.slider(_.pick(this.model.toJSON(),"value","values","range"));var e=this.$("a.ui-slider-handle");this.model.get("compareTwoMode")?(e.first().toggleClass("to-handle",!!isRtl).toggleClass("from-handle",!isRtl),e.last().toggleClass("from-handle",!!isRtl).toggleClass("to-handle",!isRtl)):e.removeClass("from-handle to-handle")},start:function(e,d){this.model.set({scrubbing:!0}),a(window).on("mousemove.wp.revisions",{view:this},function(e){var i=e.data.view,t=i.$el.offset().left,s=t,o=t+i.$el.width(),n="0",r="100%",l=a(d.handle);i.model.get("compareTwoMode")&&(i=l.parent().find(".ui-slider-handle"),l.is(i.first())?r=(o=i.last().offset().left)-s:n=(t=i.first().offset().left+i.first().width())-s),e.pageX<t?l.css("left",n):e.pageX>o?l.css("left",r):l.css("left",e.pageX-s)})},getPosition:function(e){return isRtl?this.model.revisions.length-e-1:e},slide:function(e,i){var t;if(this.model.get("compareTwoMode")){if(i.values[1]===i.values[0])return!1;isRtl&&i.values.reverse(),t={from:this.model.revisions.at(this.getPosition(i.values[0])),to:this.model.revisions.at(this.getPosition(i.values[1]))}}else t={to:this.model.revisions.at(this.getPosition(i.value))},0<this.getPosition(i.value)?t.from=this.model.revisions.at(this.getPosition(i.value)-1):t.from=void 0;i=this.model.revisions.at(this.getPosition(i.value)),this.model.get("scrubbing")&&(t.hoveredRevision=i),this.model.set(t)},stop:function(){a(window).off("mousemove.wp.revisions"),this.model.updateSliderSettings(),this.model.set({scrubbing:!1})}}),s.view.Diff=wp.Backbone.View.extend({className:"revisions-diff",template:wp.template("revisions-diff"),prepare:function(){return _.extend({fields:this.model.fields.toJSON()},this.options)}}),s.Router=Backbone.Router.extend({initialize:function(e){this.model=e.model,this.listenTo(this.model,"update:diff",_.debounce(this.updateUrl,250)),this.listenTo(this.model,"change:compareTwoMode",this.updateUrl)},baseUrl:function(e){return this.model.get("baseUrl")+e},updateUrl:function(){var e=this.model.has("from")?this.model.get("from").id:0,i=this.model.get("to").id;this.model.get("compareTwoMode")?this.navigate(this.baseUrl("?from="+e+"&to="+i),{replace:!0}):this.navigate(this.baseUrl("?revision="+i),{replace:!0})},handleRoute:function(e,i){_.isUndefined(i)||(i=this.model.revisions.get(e),e=this.model.revisions.prev(i),i=i?i.id:0,e&&e.id)}}),s.init=function(){var e;window.adminpage&&"revision-php"===window.adminpage&&(e=new s.model.FrameState({initialDiffState:{to:parseInt(s.settings.to,10),from:parseInt(s.settings.from,10),compareTwoMode:"1"===s.settings.compareTwoMode},diffData:s.settings.diffData,baseUrl:s.settings.baseUrl,postId:parseInt(s.settings.postId,10)},{revisions:new s.model.Revisions(s.settings.revisionData)}),s.view.frame=new s.view.Frame({model:e}).render())},a(s.init)}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/set-post-thumbnail.js b/wp-admin/js/set-post-thumbnail.js new file mode 100644 index 0000000..5f5f565 --- /dev/null +++ b/wp-admin/js/set-post-thumbnail.js @@ -0,0 +1,28 @@ +/** + * @output wp-admin/js/set-post-thumbnail.js + */ + +/* global ajaxurl, post_id, alert */ +/* exported WPSetAsThumbnail */ + +window.WPSetAsThumbnail = function( id, nonce ) { + var $link = jQuery('a#wp-post-thumbnail-' + id); + + $link.text( wp.i18n.__( 'Saving…' ) ); + jQuery.post(ajaxurl, { + action: 'set-post-thumbnail', post_id: post_id, thumbnail_id: id, _ajax_nonce: nonce, cookie: encodeURIComponent( document.cookie ) + }, function(str){ + var win = window.dialogArguments || opener || parent || top; + $link.text( wp.i18n.__( 'Use as featured image' ) ); + if ( str == '0' ) { + alert( wp.i18n.__( 'Could not set that as the thumbnail image. Try a different attachment.' ) ); + } else { + jQuery('a.wp-post-thumbnail').show(); + $link.text( wp.i18n.__( 'Done' ) ); + $link.fadeOut( 2000 ); + win.WPSetThumbnailID(id); + win.WPSetThumbnailHTML(str); + } + } + ); +}; diff --git a/wp-admin/js/set-post-thumbnail.min.js b/wp-admin/js/set-post-thumbnail.min.js new file mode 100644 index 0000000..638d957 --- /dev/null +++ b/wp-admin/js/set-post-thumbnail.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.WPSetAsThumbnail=function(n,t){var a=jQuery("a#wp-post-thumbnail-"+n);a.text(wp.i18n.__("Saving\u2026")),jQuery.post(ajaxurl,{action:"set-post-thumbnail",post_id:post_id,thumbnail_id:n,_ajax_nonce:t,cookie:encodeURIComponent(document.cookie)},function(t){var e=window.dialogArguments||opener||parent||top;a.text(wp.i18n.__("Use as featured image")),"0"==t?alert(wp.i18n.__("Could not set that as the thumbnail image. Try a different attachment.")):(jQuery("a.wp-post-thumbnail").show(),a.text(wp.i18n.__("Done")),a.fadeOut(2e3),e.WPSetThumbnailID(n),e.WPSetThumbnailHTML(t))})};
\ No newline at end of file diff --git a/wp-admin/js/site-health.js b/wp-admin/js/site-health.js new file mode 100644 index 0000000..5b59771 --- /dev/null +++ b/wp-admin/js/site-health.js @@ -0,0 +1,484 @@ +/** + * 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(); + // Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680 + triggerElement.trigger( 'focus' ); + + // 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 ), + '<span class="issue-count">' + count + '</span>' + ); + } else if ( 'recommended' === issue.status ) { + heading = sprintf( + _n( '%s recommended improvement', '%s recommended improvements', count ), + '<span class="issue-count">' + count + '</span>' + ); + } else if ( 'good' === issue.status ) { + heading = sprintf( + _n( '%s item with no issues detected', '%s items with no issues detected', count ), + '<span class="issue-count">' + count + '</span>' + ); + } + + 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': '<p>' + url + '</p><p>' + description + '</p>', + '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; + } + } +} ); diff --git a/wp-admin/js/site-health.min.js b/wp-admin/js/site-health.min.js new file mode 100644 index 0000000..53b14d7 --- /dev/null +++ b/wp-admin/js/site-health.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(o){var a,r=wp.i18n.__,n=wp.i18n._n,l=wp.i18n.sprintf,e=new ClipboardJS(".site-health-copy-buttons .copy-button"),c=o(".health-check-body.health-check-status-tab").length,t=o(".health-check-body.health-check-debug-tab").length,i=o("#health-check-accordion-block-wp-paths-sizes"),h=o("#adminmenu .site-health-counter"),u=o("#adminmenu .site-health-counter .count");function d(e){var t,s,a=wp.template("health-check-issue"),i=o("#health-check-issues-"+e.status);!function(e){var t,s,a,i,n={test:"string",label:"string",description:"string"},o=!0;if("object"==typeof e){for(t in n)if("object"==typeof(s=n[t]))for(a in s)i=s[a],void 0!==e[t]&&void 0!==e[t][a]&&i===typeof e[t][a]||(o=!1);else void 0!==e[t]&&s===typeof e[t]||(o=!1);return o}}(e)||(SiteHealth.site_status.issues[e.status]++,s=SiteHealth.site_status.issues[e.status],void 0===e.test&&(e.test=e.status+s),"critical"===e.status?t=l(n("%s critical issue","%s critical issues",s),'<span class="issue-count">'+s+"</span>"):"recommended"===e.status?t=l(n("%s recommended improvement","%s recommended improvements",s),'<span class="issue-count">'+s+"</span>"):"good"===e.status&&(t=l(n("%s item with no issues detected","%s items with no issues detected",s),'<span class="issue-count">'+s+"</span>")),t&&o(".site-health-issue-count-title",i).html(t),u.text(SiteHealth.site_status.issues.critical),0<parseInt(SiteHealth.site_status.issues.critical,0)?(o("#health-check-issues-critical").removeClass("hidden"),h.removeClass("count-0")):h.addClass("count-0"),0<parseInt(SiteHealth.site_status.issues.recommended,0)&&o("#health-check-issues-recommended").removeClass("hidden"),o(".issues","#health-check-issues-"+e.status).append(a(e)))}function p(){var e=o(".site-health-progress"),t=e.closest(".site-health-progress-wrapper"),s=o(".site-health-progress-label",t),a=o(".site-health-progress svg #bar"),i=parseInt(SiteHealth.site_status.issues.good,0)+parseInt(SiteHealth.site_status.issues.recommended,0)+1.5*parseInt(SiteHealth.site_status.issues.critical,0),n=.5*parseInt(SiteHealth.site_status.issues.recommended,0)+1.5*parseInt(SiteHealth.site_status.issues.critical,0),n=100-Math.ceil(n/i*100);0===i?e.addClass("hidden"):(t.removeClass("loading"),i=a.attr("r"),e=Math.PI*(2*i),a.css({strokeDashoffset:(100-(n=100<(n=n<0?0:n)?100:n))/100*e+"px"}),80<=n&&0===parseInt(SiteHealth.site_status.issues.critical,0)?(t.addClass("green").removeClass("orange"),s.text(r("Good")),m("good")):(t.addClass("orange").removeClass("green"),s.text(r("Should be improved")),m("improvable")),c&&(o.post(ajaxurl,{action:"health-check-site-status-result",_wpnonce:SiteHealth.nonce.site_status_result,counts:SiteHealth.site_status.issues}),100===n)&&(o(".site-status-all-clear").removeClass("hide"),o(".site-status-has-issues").addClass("hide")))}function g(e,t){e={status:"recommended",label:r("A test is unavailable"),badge:{color:"red",label:r("Unavailable")},description:"<p>"+e+"</p><p>"+t+"</p>",actions:""};d(wp.hooks.applyFilters("site_status_test_result",e))}function s(){var t=(new Date).getTime(),s=window.setTimeout(function(){m("waiting-for-directory-sizes")},3e3);wp.apiRequest({path:"/wp-site-health/v1/directory-sizes"}).done(function(e){var a,s;a=e||{},e=o("button.button.copy-button"),s=e.attr("data-clipboard-text"),o.each(a,function(e,t){t=t.debug||t.size;void 0!==t&&(s=s.replace(e+": loading...",e+": "+t))}),e.attr("data-clipboard-text",s),i.find("td[class]").each(function(e,t){var t=o(t),s=t.attr("class");a.hasOwnProperty(s)&&a[s].size&&t.text(a[s].size)})}).always(function(){var e=(new Date).getTime()-t;o(".health-check-wp-paths-sizes.spinner").css("visibility","hidden"),3e3<e?(e=6e3<e?0:6500-e,window.setTimeout(function(){p()},e)):window.clearTimeout(s),o(document).trigger("site-health-info-dirsizes-done")})}function m(e){if("site-health"===SiteHealth.screen)switch(e){case"good":wp.a11y.speak(r("All site health tests have finished running. Your site is looking good."));break;case"improvable":wp.a11y.speak(r("All site health tests have finished running. There are items that should be addressed."));break;case"waiting-for-directory-sizes":wp.a11y.speak(r("Running additional tests... please wait."))}}e.on("success",function(e){var t=o(e.trigger),s=o(".success",t.closest("div"));e.clearSelection(),t.trigger("focus"),clearTimeout(a),s.removeClass("hidden"),a=setTimeout(function(){s.addClass("hidden")},3e3),wp.a11y.speak(r("Site information has been copied to your clipboard."))}),o(".health-check-accordion").on("click",".health-check-accordion-trigger",function(){"true"===o(this).attr("aria-expanded")?(o(this).attr("aria-expanded","false"),o("#"+o(this).attr("aria-controls")).attr("hidden",!0)):(o(this).attr("aria-expanded","true"),o("#"+o(this).attr("aria-controls")).attr("hidden",!1))}),o(".site-health-view-passed").on("click",function(){var e=o("#health-check-issues-good");e.toggleClass("hidden"),o(this).attr("aria-expanded",!e.hasClass("hidden"))}),"undefined"!=typeof SiteHealth&&(0===SiteHealth.site_status.direct.length&&0===SiteHealth.site_status.async.length?p():SiteHealth.site_status.issues={good:0,recommended:0,critical:0},0<SiteHealth.site_status.direct.length&&o.each(SiteHealth.site_status.direct,function(){d(this)}),(0<SiteHealth.site_status.async.length?function t(){var s=!0;1<=SiteHealth.site_status.async.length&&o.each(SiteHealth.site_status.async,function(){var e={action:"health-check-"+this.test.replace("_","-"),_wpnonce:SiteHealth.nonce.site_status};return!!this.completed||(s=!1,this.completed=!0,(void 0!==this.has_rest&&this.has_rest?wp.apiRequest({url:wp.url.addQueryArgs(this.test,{_locale:"user"}),headers:this.headers}).done(function(e){d(wp.hooks.applyFilters("site_status_test_result",e))}).fail(function(e){e=void 0!==e.responseJSON&&void 0!==e.responseJSON.message?e.responseJSON.message:r("No details available"),g(this.url,e)}):o.post(ajaxurl,e).done(function(e){d(wp.hooks.applyFilters("site_status_test_result",e.data))}).fail(function(e){e=void 0!==e.responseJSON&&void 0!==e.responseJSON.message?e.responseJSON.message:r("No details available"),g(this.url,e)})).always(function(){t()}),!1)}),s&&p()}:p)()),t&&(i.length?s:p)(),o(".health-check-offscreen-nav-wrapper").on("click",function(){o(this).toggleClass("visible")})});
\ No newline at end of file diff --git a/wp-admin/js/svg-painter.js b/wp-admin/js/svg-painter.js new file mode 100644 index 0000000..a356735 --- /dev/null +++ b/wp-admin/js/svg-painter.js @@ -0,0 +1,238 @@ +/** + * Attempt to re-color SVG icons used in the admin menu or the toolbar + * + * @output wp-admin/js/svg-painter.js + */ + +window.wp = window.wp || {}; + +wp.svgPainter = ( function( $, window, document, undefined ) { + 'use strict'; + var selector, base64, painter, + colorscheme = {}, + elements = []; + + $( function() { + // Detection for browser SVG capability. + if ( document.implementation.hasFeature( 'http://www.w3.org/TR/SVG11/feature#Image', '1.1' ) ) { + $( document.body ).removeClass( 'no-svg' ).addClass( 'svg' ); + wp.svgPainter.init(); + } + }); + + /** + * Needed only for IE9 + * + * Based on jquery.base64.js 0.0.3 - https://github.com/yckart/jquery.base64.js + * + * Based on: https://gist.github.com/Yaffle/1284012 + * + * Copyright (c) 2012 Yannick Albert (http://yckart.com) + * Licensed under the MIT license + * http://www.opensource.org/licenses/mit-license.php + */ + base64 = ( function() { + var c, + b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + a256 = '', + r64 = [256], + r256 = [256], + i = 0; + + function init() { + while( i < 256 ) { + c = String.fromCharCode(i); + a256 += c; + r256[i] = i; + r64[i] = b64.indexOf(c); + ++i; + } + } + + function code( s, discard, alpha, beta, w1, w2 ) { + var tmp, length, + buffer = 0, + i = 0, + result = '', + bitsInBuffer = 0; + + s = String(s); + length = s.length; + + while( i < length ) { + c = s.charCodeAt(i); + c = c < 256 ? alpha[c] : -1; + + buffer = ( buffer << w1 ) + c; + bitsInBuffer += w1; + + while( bitsInBuffer >= w2 ) { + bitsInBuffer -= w2; + tmp = buffer >> bitsInBuffer; + result += beta.charAt(tmp); + buffer ^= tmp << bitsInBuffer; + } + ++i; + } + + if ( ! discard && bitsInBuffer > 0 ) { + result += beta.charAt( buffer << ( w2 - bitsInBuffer ) ); + } + + return result; + } + + function btoa( plain ) { + if ( ! c ) { + init(); + } + + plain = code( plain, false, r256, b64, 8, 6 ); + return plain + '===='.slice( ( plain.length % 4 ) || 4 ); + } + + function atob( coded ) { + var i; + + if ( ! c ) { + init(); + } + + coded = coded.replace( /[^A-Za-z0-9\+\/\=]/g, '' ); + coded = String(coded).split('='); + i = coded.length; + + do { + --i; + coded[i] = code( coded[i], true, r64, a256, 6, 8 ); + } while ( i > 0 ); + + coded = coded.join(''); + return coded; + } + + return { + atob: atob, + btoa: btoa + }; + })(); + + return { + init: function() { + painter = this; + selector = $( '#adminmenu .wp-menu-image, #wpadminbar .ab-item' ); + + this.setColors(); + this.findElements(); + this.paint(); + }, + + setColors: function( colors ) { + if ( typeof colors === 'undefined' && typeof window._wpColorScheme !== 'undefined' ) { + colors = window._wpColorScheme; + } + + if ( colors && colors.icons && colors.icons.base && colors.icons.current && colors.icons.focus ) { + colorscheme = colors.icons; + } + }, + + findElements: function() { + selector.each( function() { + var $this = $(this), bgImage = $this.css( 'background-image' ); + + if ( bgImage && bgImage.indexOf( 'data:image/svg+xml;base64' ) != -1 ) { + elements.push( $this ); + } + }); + }, + + paint: function() { + // Loop through all elements. + $.each( elements, function( index, $element ) { + var $menuitem = $element.parent().parent(); + + if ( $menuitem.hasClass( 'current' ) || $menuitem.hasClass( 'wp-has-current-submenu' ) ) { + // Paint icon in 'current' color. + painter.paintElement( $element, 'current' ); + } else { + // Paint icon in base color. + painter.paintElement( $element, 'base' ); + + // Set hover callbacks. + $menuitem.on( 'mouseenter', function() { + painter.paintElement( $element, 'focus' ); + } ).on( 'mouseleave', function() { + // Match the delay from hoverIntent. + window.setTimeout( function() { + painter.paintElement( $element, 'base' ); + }, 100 ); + } ); + } + }); + }, + + paintElement: function( $element, colorType ) { + var xml, encoded, color; + + if ( ! colorType || ! colorscheme.hasOwnProperty( colorType ) ) { + return; + } + + color = colorscheme[ colorType ]; + + // Only accept hex colors: #101 or #101010. + if ( ! color.match( /^(#[0-9a-f]{3}|#[0-9a-f]{6})$/i ) ) { + return; + } + + xml = $element.data( 'wp-ui-svg-' + color ); + + if ( xml === 'none' ) { + return; + } + + if ( ! xml ) { + encoded = $element.css( 'background-image' ).match( /.+data:image\/svg\+xml;base64,([A-Za-z0-9\+\/\=]+)/ ); + + if ( ! encoded || ! encoded[1] ) { + $element.data( 'wp-ui-svg-' + color, 'none' ); + return; + } + + try { + if ( 'atob' in window ) { + xml = window.atob( encoded[1] ); + } else { + xml = base64.atob( encoded[1] ); + } + } catch ( error ) {} + + if ( xml ) { + // Replace `fill` attributes. + xml = xml.replace( /fill="(.+?)"/g, 'fill="' + color + '"'); + + // Replace `style` attributes. + xml = xml.replace( /style="(.+?)"/g, 'style="fill:' + color + '"'); + + // Replace `fill` properties in `<style>` tags. + xml = xml.replace( /fill:.*?;/g, 'fill: ' + color + ';'); + + if ( 'btoa' in window ) { + xml = window.btoa( xml ); + } else { + xml = base64.btoa( xml ); + } + + $element.data( 'wp-ui-svg-' + color, xml ); + } else { + $element.data( 'wp-ui-svg-' + color, 'none' ); + return; + } + } + + $element.attr( 'style', 'background-image: url("data:image/svg+xml;base64,' + xml + '") !important;' ); + } + }; + +})( jQuery, window, document ); diff --git a/wp-admin/js/svg-painter.min.js b/wp-admin/js/svg-painter.min.js new file mode 100644 index 0000000..a6f012e --- /dev/null +++ b/wp-admin/js/svg-painter.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},wp.svgPainter=function(e,i,n){"use strict";var t,o,a,m,r,s,c,u,l,f={},g=[];function p(){for(;l<256;)m=String.fromCharCode(l),s+=m,u[l]=l,c[l]=r.indexOf(m),++l}function d(n,t,e,a,i,o){for(var r,s=0,c=0,u="",l=0,f=(n=String(n)).length;c<f;){for(s=(s<<i)+(m=(m=n.charCodeAt(c))<256?e[m]:-1),l+=i;o<=l;)l-=o,u+=a.charAt(r=s>>l),s^=r<<l;++c}return!t&&0<l&&(u+=a.charAt(s<<o-l)),u}return e(function(){n.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image","1.1")&&(e(n.body).removeClass("no-svg").addClass("svg"),wp.svgPainter.init())}),r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s="",c=[256],u=[256],l=0,o={atob:function(n){var t;for(m||p(),n=n.replace(/[^A-Za-z0-9\+\/\=]/g,""),t=(n=String(n).split("=")).length;n[--t]=d(n[t],!0,c,s,6,8),0<t;);return n=n.join("")},btoa:function(n){return m||p(),(n=d(n,!1,u,r,8,6))+"====".slice(n.length%4||4)}},{init:function(){a=this,t=e("#adminmenu .wp-menu-image, #wpadminbar .ab-item"),this.setColors(),this.findElements(),this.paint()},setColors:function(n){(n=void 0===n&&void 0!==i._wpColorScheme?i._wpColorScheme:n)&&n.icons&&n.icons.base&&n.icons.current&&n.icons.focus&&(f=n.icons)},findElements:function(){t.each(function(){var n=e(this),t=n.css("background-image");t&&-1!=t.indexOf("data:image/svg+xml;base64")&&g.push(n)})},paint:function(){e.each(g,function(n,t){var e=t.parent().parent();e.hasClass("current")||e.hasClass("wp-has-current-submenu")?a.paintElement(t,"current"):(a.paintElement(t,"base"),e.on("mouseenter",function(){a.paintElement(t,"focus")}).on("mouseleave",function(){i.setTimeout(function(){a.paintElement(t,"base")},100)}))})},paintElement:function(n,t){var e,a;if(t&&f.hasOwnProperty(t)&&(t=f[t]).match(/^(#[0-9a-f]{3}|#[0-9a-f]{6})$/i)&&"none"!==(e=n.data("wp-ui-svg-"+t))){if(!e){if(!(a=n.css("background-image").match(/.+data:image\/svg\+xml;base64,([A-Za-z0-9\+\/\=]+)/))||!a[1])return void n.data("wp-ui-svg-"+t,"none");try{e=("atob"in i?i:o).atob(a[1])}catch(n){}if(!e)return void n.data("wp-ui-svg-"+t,"none");e=(e=(e=e.replace(/fill="(.+?)"/g,'fill="'+t+'"')).replace(/style="(.+?)"/g,'style="fill:'+t+'"')).replace(/fill:.*?;/g,"fill: "+t+";"),e=("btoa"in i?i:o).btoa(e),n.data("wp-ui-svg-"+t,e)}n.attr("style",'background-image: url("data:image/svg+xml;base64,'+e+'") !important;')}}}}(jQuery,window,document);
\ No newline at end of file diff --git a/wp-admin/js/tags-box.js b/wp-admin/js/tags-box.js new file mode 100644 index 0000000..99d6646 --- /dev/null +++ b/wp-admin/js/tags-box.js @@ -0,0 +1,440 @@ +/** + * @output wp-admin/js/tags-box.js + */ + +/* jshint curly: false, eqeqeq: false */ +/* global ajaxurl, tagBox, array_unique_noempty */ + +( function( $ ) { + var tagDelimiter = wp.i18n._x( ',', 'tag delimiter' ) || ','; + + /** + * Filters unique items and returns a new array. + * + * Filters all items from an array into a new array containing only the unique + * items. This also excludes whitespace or empty values. + * + * @since 2.8.0 + * + * @global + * + * @param {Array} array The array to filter through. + * + * @return {Array} A new array containing only the unique items. + */ + window.array_unique_noempty = function( array ) { + var out = []; + + // Trim the values and ensure they are unique. + $.each( array, function( key, val ) { + val = val || ''; + val = val.trim(); + + if ( val && $.inArray( val, out ) === -1 ) { + out.push( val ); + } + } ); + + return out; + }; + + /** + * The TagBox object. + * + * Contains functions to create and manage tags that can be associated with a + * post. + * + * @since 2.9.0 + * + * @global + */ + window.tagBox = { + /** + * Cleans up tags by removing redundant characters. + * + * @since 2.9.0 + * + * @memberOf tagBox + * + * @param {string} tags Comma separated tags that need to be cleaned up. + * + * @return {string} The cleaned up tags. + */ + clean : function( tags ) { + if ( ',' !== tagDelimiter ) { + tags = tags.replace( new RegExp( tagDelimiter, 'g' ), ',' ); + } + + tags = tags.replace(/\s*,\s*/g, ',').replace(/,+/g, ',').replace(/[,\s]+$/, '').replace(/^[,\s]+/, ''); + + if ( ',' !== tagDelimiter ) { + tags = tags.replace( /,/g, tagDelimiter ); + } + + return tags; + }, + + /** + * Parses tags and makes them editable. + * + * @since 2.9.0 + * + * @memberOf tagBox + * + * @param {Object} el The tag element to retrieve the ID from. + * + * @return {boolean} Always returns false. + */ + parseTags : function(el) { + var id = el.id, + num = id.split('-check-num-')[1], + taxbox = $(el).closest('.tagsdiv'), + thetags = taxbox.find('.the-tags'), + current_tags = thetags.val().split( tagDelimiter ), + new_tags = []; + + delete current_tags[num]; + + // Sanitize the current tags and push them as if they're new tags. + $.each( current_tags, function( key, val ) { + val = val || ''; + val = val.trim(); + if ( val ) { + new_tags.push( val ); + } + }); + + thetags.val( this.clean( new_tags.join( tagDelimiter ) ) ); + + this.quickClicks( taxbox ); + return false; + }, + + /** + * Creates clickable links, buttons and fields for adding or editing tags. + * + * @since 2.9.0 + * + * @memberOf tagBox + * + * @param {Object} el The container HTML element. + * + * @return {void} + */ + quickClicks : function( el ) { + var thetags = $('.the-tags', el), + tagchecklist = $('.tagchecklist', el), + id = $(el).attr('id'), + current_tags, disabled; + + if ( ! thetags.length ) + return; + + disabled = thetags.prop('disabled'); + + current_tags = thetags.val().split( tagDelimiter ); + tagchecklist.empty(); + + /** + * Creates a delete button if tag editing is enabled, before adding it to the tag list. + * + * @since 2.5.0 + * + * @memberOf tagBox + * + * @param {string} key The index of the current tag. + * @param {string} val The value of the current tag. + * + * @return {void} + */ + $.each( current_tags, function( key, val ) { + var listItem, xbutton; + + val = val || ''; + val = val.trim(); + + if ( ! val ) + return; + + // Create a new list item, and ensure the text is properly escaped. + listItem = $( '<li />' ).text( val ); + + // If tags editing isn't disabled, create the X button. + if ( ! disabled ) { + /* + * Build the X buttons, hide the X icon with aria-hidden and + * use visually hidden text for screen readers. + */ + xbutton = $( '<button type="button" id="' + id + '-check-num-' + key + '" class="ntdelbutton">' + + '<span class="remove-tag-icon" aria-hidden="true"></span>' + + '<span class="screen-reader-text">' + wp.i18n.__( 'Remove term:' ) + ' ' + listItem.html() + '</span>' + + '</button>' ); + + /** + * Handles the click and keypress event of the tag remove button. + * + * Makes sure the focus ends up in the tag input field when using + * the keyboard to delete the tag. + * + * @since 4.2.0 + * + * @param {Event} e The click or keypress event to handle. + * + * @return {void} + */ + xbutton.on( 'click keypress', function( e ) { + // On click or when using the Enter/Spacebar keys. + if ( 'click' === e.type || 13 === e.keyCode || 32 === e.keyCode ) { + /* + * When using the keyboard, move focus back to the + * add new tag field. Note: when releasing the pressed + * key this will fire the `keyup` event on the input. + */ + if ( 13 === e.keyCode || 32 === e.keyCode ) { + $( this ).closest( '.tagsdiv' ).find( 'input.newtag' ).trigger( 'focus' ); + } + + tagBox.userAction = 'remove'; + tagBox.parseTags( this ); + } + }); + + listItem.prepend( ' ' ).prepend( xbutton ); + } + + // Append the list item to the tag list. + tagchecklist.append( listItem ); + }); + + // The buttons list is built now, give feedback to screen reader users. + tagBox.screenReadersMessage(); + }, + + /** + * Adds a new tag. + * + * Also ensures that the quick links are properly generated. + * + * @since 2.9.0 + * + * @memberOf tagBox + * + * @param {Object} el The container HTML element. + * @param {Object|boolean} a When this is an HTML element the text of that + * element will be used for the new tag. + * @param {number|boolean} f If this value is not passed then the tag input + * field is focused. + * + * @return {boolean} Always returns false. + */ + flushTags : function( el, a, f ) { + var tagsval, newtags, text, + tags = $( '.the-tags', el ), + newtag = $( 'input.newtag', el ); + + a = a || false; + + text = a ? $(a).text() : newtag.val(); + + /* + * Return if there's no new tag or if the input field is empty. + * Note: when using the keyboard to add tags, focus is moved back to + * the input field and the `keyup` event attached on this field will + * fire when releasing the pressed key. Checking also for the field + * emptiness avoids to set the tags and call quickClicks() again. + */ + if ( 'undefined' == typeof( text ) || '' === text ) { + return false; + } + + tagsval = tags.val(); + newtags = tagsval ? tagsval + tagDelimiter + text : text; + + newtags = this.clean( newtags ); + newtags = array_unique_noempty( newtags.split( tagDelimiter ) ).join( tagDelimiter ); + tags.val( newtags ); + this.quickClicks( el ); + + if ( ! a ) + newtag.val(''); + if ( 'undefined' == typeof( f ) ) + newtag.trigger( 'focus' ); + + return false; + }, + + /** + * Retrieves the available tags and creates a tagcloud. + * + * Retrieves the available tags from the database and creates an interactive + * tagcloud. Clicking a tag will add it. + * + * @since 2.9.0 + * + * @memberOf tagBox + * + * @param {string} id The ID to extract the taxonomy from. + * + * @return {void} + */ + get : function( id ) { + var tax = id.substr( id.indexOf('-') + 1 ); + + /** + * Puts a received tag cloud into a DOM element. + * + * The tag cloud HTML is generated on the server. + * + * @since 2.9.0 + * + * @param {number|string} r The response message from the Ajax call. + * @param {string} stat The status of the Ajax request. + * + * @return {void} + */ + $.post( ajaxurl, { 'action': 'get-tagcloud', 'tax': tax }, function( r, stat ) { + if ( 0 === r || 'success' != stat ) { + return; + } + + r = $( '<div id="tagcloud-' + tax + '" class="the-tagcloud">' + r + '</div>' ); + + /** + * Adds a new tag when a tag in the tagcloud is clicked. + * + * @since 2.9.0 + * + * @return {boolean} Returns false to prevent the default action. + */ + $( 'a', r ).on( 'click', function() { + tagBox.userAction = 'add'; + tagBox.flushTags( $( '#' + tax ), this ); + return false; + }); + + $( '#' + id ).after( r ); + }); + }, + + /** + * Track the user's last action. + * + * @since 4.7.0 + */ + userAction: '', + + /** + * Dispatches an audible message to screen readers. + * + * This will inform the user when a tag has been added or removed. + * + * @since 4.7.0 + * + * @return {void} + */ + screenReadersMessage: function() { + var message; + + switch ( this.userAction ) { + case 'remove': + message = wp.i18n.__( 'Term removed.' ); + break; + + case 'add': + message = wp.i18n.__( 'Term added.' ); + break; + + default: + return; + } + + window.wp.a11y.speak( message, 'assertive' ); + }, + + /** + * Initializes the tags box by setting up the links, buttons. Sets up event + * handling. + * + * This includes handling of pressing the enter key in the input field and the + * retrieval of tag suggestions. + * + * @since 2.9.0 + * + * @memberOf tagBox + * + * @return {void} + */ + init : function() { + var ajaxtag = $('div.ajaxtag'); + + $('.tagsdiv').each( function() { + tagBox.quickClicks( this ); + }); + + $( '.tagadd', ajaxtag ).on( 'click', function() { + tagBox.userAction = 'add'; + tagBox.flushTags( $( this ).closest( '.tagsdiv' ) ); + }); + + /** + * Handles pressing enter on the new tag input field. + * + * Prevents submitting the post edit form. Uses `keypress` to take + * into account Input Method Editor (IME) converters. + * + * @since 2.9.0 + * + * @param {Event} event The keypress event that occurred. + * + * @return {void} + */ + $( 'input.newtag', ajaxtag ).on( 'keypress', function( event ) { + if ( 13 == event.which ) { + tagBox.userAction = 'add'; + tagBox.flushTags( $( this ).closest( '.tagsdiv' ) ); + event.preventDefault(); + event.stopPropagation(); + } + }).each( function( i, element ) { + $( element ).wpTagsSuggest(); + }); + + /** + * Before a post is saved the value currently in the new tag input field will be + * added as a tag. + * + * @since 2.9.0 + * + * @return {void} + */ + $('#post').on( 'submit', function(){ + $('div.tagsdiv').each( function() { + tagBox.flushTags(this, false, 1); + }); + }); + + /** + * Handles clicking on the tag cloud link. + * + * Makes sure the ARIA attributes are set correctly. + * + * @since 2.9.0 + * + * @return {void} + */ + $('.tagcloud-link').on( 'click', function(){ + // On the first click, fetch the tag cloud and insert it in the DOM. + tagBox.get( $( this ).attr( 'id' ) ); + // Update button state, remove previous click event and attach a new one to toggle the cloud. + $( this ) + .attr( 'aria-expanded', 'true' ) + .off() + .on( 'click', function() { + $( this ) + .attr( 'aria-expanded', 'false' === $( this ).attr( 'aria-expanded' ) ? 'true' : 'false' ) + .siblings( '.the-tagcloud' ).toggle(); + }); + }); + } + }; +}( jQuery )); diff --git a/wp-admin/js/tags-box.min.js b/wp-admin/js/tags-box.min.js new file mode 100644 index 0000000..b81f949 --- /dev/null +++ b/wp-admin/js/tags-box.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(o){var r=wp.i18n._x(",","tag delimiter")||",";window.array_unique_noempty=function(t){var a=[];return o.each(t,function(t,e){(e=(e=e||"").trim())&&-1===o.inArray(e,a)&&a.push(e)}),a},window.tagBox={clean:function(t){return t=(t=","!==r?t.replace(new RegExp(r,"g"),","):t).replace(/\s*,\s*/g,",").replace(/,+/g,",").replace(/[,\s]+$/,"").replace(/^[,\s]+/,""),t=","!==r?t.replace(/,/g,r):t},parseTags:function(t){var e=t.id.split("-check-num-")[1],t=o(t).closest(".tagsdiv"),a=t.find(".the-tags"),i=a.val().split(r),n=[];return delete i[e],o.each(i,function(t,e){(e=(e=e||"").trim())&&n.push(e)}),a.val(this.clean(n.join(r))),this.quickClicks(t),!1},quickClicks:function(t){var a,e=o(".the-tags",t),i=o(".tagchecklist",t),n=o(t).attr("id");e.length&&(a=e.prop("disabled"),t=e.val().split(r),i.empty(),o.each(t,function(t,e){(e=(e=e||"").trim())&&(e=o("<li />").text(e),a||((t=o('<button type="button" id="'+n+"-check-num-"+t+'" class="ntdelbutton"><span class="remove-tag-icon" aria-hidden="true"></span><span class="screen-reader-text">'+wp.i18n.__("Remove term:")+" "+e.html()+"</span></button>")).on("click keypress",function(t){"click"!==t.type&&13!==t.keyCode&&32!==t.keyCode||(13!==t.keyCode&&32!==t.keyCode||o(this).closest(".tagsdiv").find("input.newtag").trigger("focus"),tagBox.userAction="remove",tagBox.parseTags(this))}),e.prepend(" ").prepend(t)),i.append(e))}),tagBox.screenReadersMessage())},flushTags:function(t,e,a){var i,n,s=o(".the-tags",t),c=o("input.newtag",t);return void 0!==(n=(e=e||!1)?o(e).text():c.val())&&""!==n&&(i=s.val(),i=this.clean(i=i?i+r+n:n),i=array_unique_noempty(i.split(r)).join(r),s.val(i),this.quickClicks(t),e||c.val(""),void 0===a)&&c.trigger("focus"),!1},get:function(a){var i=a.substr(a.indexOf("-")+1);o.post(ajaxurl,{action:"get-tagcloud",tax:i},function(t,e){0!==t&&"success"==e&&(t=o('<div id="tagcloud-'+i+'" class="the-tagcloud">'+t+"</div>"),o("a",t).on("click",function(){return tagBox.userAction="add",tagBox.flushTags(o("#"+i),this),!1}),o("#"+a).after(t))})},userAction:"",screenReadersMessage:function(){var t;switch(this.userAction){case"remove":t=wp.i18n.__("Term removed.");break;case"add":t=wp.i18n.__("Term added.");break;default:return}window.wp.a11y.speak(t,"assertive")},init:function(){var t=o("div.ajaxtag");o(".tagsdiv").each(function(){tagBox.quickClicks(this)}),o(".tagadd",t).on("click",function(){tagBox.userAction="add",tagBox.flushTags(o(this).closest(".tagsdiv"))}),o("input.newtag",t).on("keypress",function(t){13==t.which&&(tagBox.userAction="add",tagBox.flushTags(o(this).closest(".tagsdiv")),t.preventDefault(),t.stopPropagation())}).each(function(t,e){o(e).wpTagsSuggest()}),o("#post").on("submit",function(){o("div.tagsdiv").each(function(){tagBox.flushTags(this,!1,1)})}),o(".tagcloud-link").on("click",function(){tagBox.get(o(this).attr("id")),o(this).attr("aria-expanded","true").off().on("click",function(){o(this).attr("aria-expanded","false"===o(this).attr("aria-expanded")?"true":"false").siblings(".the-tagcloud").toggle()})})}}}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/tags-suggest.js b/wp-admin/js/tags-suggest.js new file mode 100644 index 0000000..f93396a --- /dev/null +++ b/wp-admin/js/tags-suggest.js @@ -0,0 +1,209 @@ +/** + * Default settings for jQuery UI Autocomplete for use with non-hierarchical taxonomies. + * + * @output wp-admin/js/tags-suggest.js + */ +( function( $ ) { + if ( typeof window.uiAutocompleteL10n === 'undefined' ) { + return; + } + + var tempID = 0; + var separator = wp.i18n._x( ',', 'tag delimiter' ) || ','; + + function split( val ) { + return val.split( new RegExp( separator + '\\s*' ) ); + } + + function getLast( term ) { + return split( term ).pop(); + } + + /** + * Add UI Autocomplete to an input or textarea element with presets for use + * with non-hierarchical taxonomies. + * + * Example: `$( element ).wpTagsSuggest( options )`. + * + * The taxonomy can be passed in a `data-wp-taxonomy` attribute on the element or + * can be in `options.taxonomy`. + * + * @since 4.7.0 + * + * @param {Object} options Options that are passed to UI Autocomplete. Can be used to override the default settings. + * @return {Object} jQuery instance. + */ + $.fn.wpTagsSuggest = function( options ) { + var cache; + var last; + var $element = $( this ); + + // Do not initialize if the element doesn't exist. + if ( ! $element.length ) { + return this; + } + + options = options || {}; + + var taxonomy = options.taxonomy || $element.attr( 'data-wp-taxonomy' ) || 'post_tag'; + + delete( options.taxonomy ); + + options = $.extend( { + source: function( request, response ) { + var term; + + if ( last === request.term ) { + response( cache ); + return; + } + + term = getLast( request.term ); + + $.get( window.ajaxurl, { + action: 'ajax-tag-search', + tax: taxonomy, + q: term, + number: 20 + } ).always( function() { + $element.removeClass( 'ui-autocomplete-loading' ); // UI fails to remove this sometimes? + } ).done( function( data ) { + var tagName; + var tags = []; + + if ( data ) { + data = data.split( '\n' ); + + for ( tagName in data ) { + var id = ++tempID; + + tags.push({ + id: id, + name: data[tagName] + }); + } + + cache = tags; + response( tags ); + } else { + response( tags ); + } + } ); + + last = request.term; + }, + focus: function( event, ui ) { + $element.attr( 'aria-activedescendant', 'wp-tags-autocomplete-' + ui.item.id ); + + // Don't empty the input field when using the arrow keys + // to highlight items. See api.jqueryui.com/autocomplete/#event-focus + event.preventDefault(); + }, + select: function( event, ui ) { + var tags = split( $element.val() ); + // Remove the last user input. + tags.pop(); + // Append the new tag and an empty element to get one more separator at the end. + tags.push( ui.item.name, '' ); + + $element.val( tags.join( separator + ' ' ) ); + + if ( $.ui.keyCode.TAB === event.keyCode ) { + // Audible confirmation message when a tag has been selected. + window.wp.a11y.speak( wp.i18n.__( 'Term selected.' ), 'assertive' ); + event.preventDefault(); + } else if ( $.ui.keyCode.ENTER === event.keyCode ) { + // If we're in the edit post Tags meta box, add the tag. + if ( window.tagBox ) { + window.tagBox.userAction = 'add'; + window.tagBox.flushTags( $( this ).closest( '.tagsdiv' ) ); + } + + // Do not close Quick Edit / Bulk Edit. + event.preventDefault(); + event.stopPropagation(); + } + + return false; + }, + open: function() { + $element.attr( 'aria-expanded', 'true' ); + }, + close: function() { + $element.attr( 'aria-expanded', 'false' ); + }, + minLength: 2, + position: { + my: 'left top+2', + at: 'left bottom', + collision: 'none' + }, + messages: { + noResults: window.uiAutocompleteL10n.noResults, + results: function( number ) { + if ( number > 1 ) { + return window.uiAutocompleteL10n.manyResults.replace( '%d', number ); + } + + return window.uiAutocompleteL10n.oneResult; + } + } + }, options ); + + $element.on( 'keydown', function() { + $element.removeAttr( 'aria-activedescendant' ); + } ); + + $element.autocomplete( options ); + + // Ensure the autocomplete instance exists. + if ( ! $element.autocomplete( 'instance' ) ) { + return this; + } + + $element.autocomplete( 'instance' )._renderItem = function( ul, item ) { + return $( '<li role="option" id="wp-tags-autocomplete-' + item.id + '">' ) + .text( item.name ) + .appendTo( ul ); + }; + + $element.attr( { + 'role': 'combobox', + 'aria-autocomplete': 'list', + 'aria-expanded': 'false', + 'aria-owns': $element.autocomplete( 'widget' ).attr( 'id' ) + } ) + .on( 'focus', function() { + var inputValue = split( $element.val() ).pop(); + + // Don't trigger a search if the field is empty. + // Also, avoids screen readers announce `No search results`. + if ( inputValue ) { + $element.autocomplete( 'search' ); + } + } ); + + // Returns a jQuery object containing the menu element. + $element.autocomplete( 'widget' ) + .addClass( 'wp-tags-autocomplete' ) + .attr( 'role', 'listbox' ) + .removeAttr( 'tabindex' ) // Remove the `tabindex=0` attribute added by jQuery UI. + + /* + * Looks like Safari and VoiceOver need an `aria-selected` attribute. See ticket #33301. + * The `menufocus` and `menublur` events are the same events used to add and remove + * the `ui-state-focus` CSS class on the menu items. See jQuery UI Menu Widget. + */ + .on( 'menufocus', function( event, ui ) { + ui.item.attr( 'aria-selected', 'true' ); + }) + .on( 'menublur', function() { + // The `menublur` event returns an object where the item is `null`, + // so we need to find the active item with other means. + $( this ).find( '[aria-selected="true"]' ).removeAttr( 'aria-selected' ); + }); + + return this; + }; + +}( jQuery ) ); diff --git a/wp-admin/js/tags-suggest.min.js b/wp-admin/js/tags-suggest.min.js new file mode 100644 index 0000000..bbea7bb --- /dev/null +++ b/wp-admin/js/tags-suggest.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(u){var s,a;function l(e){return e.split(new RegExp(a+"\\s*"))}void 0!==window.uiAutocompleteL10n&&(s=0,a=wp.i18n._x(",","tag delimiter")||",",u.fn.wpTagsSuggest=function(e){var i,o,n,r=u(this);return r.length&&(n=(e=e||{}).taxonomy||r.attr("data-wp-taxonomy")||"post_tag",delete e.taxonomy,e=u.extend({source:function(e,a){var t;o===e.term?a(i):(t=l(e.term).pop(),u.get(window.ajaxurl,{action:"ajax-tag-search",tax:n,q:t,number:20}).always(function(){r.removeClass("ui-autocomplete-loading")}).done(function(e){var t,o=[];if(e){for(t in e=e.split("\n")){var n=++s;o.push({id:n,name:e[t]})}a(i=o)}else a(o)}),o=e.term)},focus:function(e,t){r.attr("aria-activedescendant","wp-tags-autocomplete-"+t.item.id),e.preventDefault()},select:function(e,t){var o=l(r.val());return o.pop(),o.push(t.item.name,""),r.val(o.join(a+" ")),u.ui.keyCode.TAB===e.keyCode?(window.wp.a11y.speak(wp.i18n.__("Term selected."),"assertive"),e.preventDefault()):u.ui.keyCode.ENTER===e.keyCode&&(window.tagBox&&(window.tagBox.userAction="add",window.tagBox.flushTags(u(this).closest(".tagsdiv"))),e.preventDefault(),e.stopPropagation()),!1},open:function(){r.attr("aria-expanded","true")},close:function(){r.attr("aria-expanded","false")},minLength:2,position:{my:"left top+2",at:"left bottom",collision:"none"},messages:{noResults:window.uiAutocompleteL10n.noResults,results:function(e){return 1<e?window.uiAutocompleteL10n.manyResults.replace("%d",e):window.uiAutocompleteL10n.oneResult}}},e),r.on("keydown",function(){r.removeAttr("aria-activedescendant")}),r.autocomplete(e),r.autocomplete("instance"))&&(r.autocomplete("instance")._renderItem=function(e,t){return u('<li role="option" id="wp-tags-autocomplete-'+t.id+'">').text(t.name).appendTo(e)},r.attr({role:"combobox","aria-autocomplete":"list","aria-expanded":"false","aria-owns":r.autocomplete("widget").attr("id")}).on("focus",function(){l(r.val()).pop()&&r.autocomplete("search")}),r.autocomplete("widget").addClass("wp-tags-autocomplete").attr("role","listbox").removeAttr("tabindex").on("menufocus",function(e,t){t.item.attr("aria-selected","true")}).on("menublur",function(){u(this).find('[aria-selected="true"]').removeAttr("aria-selected")})),this})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/tags.js b/wp-admin/js/tags.js new file mode 100644 index 0000000..2e55e2e --- /dev/null +++ b/wp-admin/js/tags.js @@ -0,0 +1,167 @@ +/** + * Contains logic for deleting and adding tags. + * + * For deleting tags it makes a request to the server to delete the tag. + * For adding tags it makes a request to the server to add the tag. + * + * @output wp-admin/js/tags.js + */ + + /* global ajaxurl, wpAjax, showNotice, validateForm */ + +jQuery( function($) { + + var addingTerm = false; + + /** + * Adds an event handler to the delete term link on the term overview page. + * + * Cancels default event handling and event bubbling. + * + * @since 2.8.0 + * + * @return {boolean} Always returns false to cancel the default event handling. + */ + $( '#the-list' ).on( 'click', '.delete-tag', function() { + var t = $(this), tr = t.parents('tr'), r = true, data; + + if ( 'undefined' != showNotice ) + r = showNotice.warn(); + + if ( r ) { + data = t.attr('href').replace(/[^?]*\?/, '').replace(/action=delete/, 'action=delete-tag'); + + /** + * Makes a request to the server to delete the term that corresponds to the + * delete term button. + * + * @param {string} r The response from the server. + * + * @return {void} + */ + $.post(ajaxurl, data, function(r){ + if ( '1' == r ) { + $('#ajax-response').empty(); + tr.fadeOut('normal', function(){ tr.remove(); }); + + /** + * Removes the term from the parent box and the tag cloud. + * + * `data.match(/tag_ID=(\d+)/)[1]` matches the term ID from the data variable. + * This term ID is then used to select the relevant HTML elements: + * The parent box and the tag cloud. + */ + $('select#parent option[value="' + data.match(/tag_ID=(\d+)/)[1] + '"]').remove(); + $('a.tag-link-' + data.match(/tag_ID=(\d+)/)[1]).remove(); + + } else if ( '-1' == r ) { + $('#ajax-response').empty().append('<div class="error"><p>' + wp.i18n.__( 'Sorry, you are not allowed to do that.' ) + '</p></div>'); + tr.children().css('backgroundColor', ''); + + } else { + $('#ajax-response').empty().append('<div class="error"><p>' + wp.i18n.__( 'Something went wrong.' ) + '</p></div>'); + tr.children().css('backgroundColor', ''); + } + }); + + tr.children().css('backgroundColor', '#f33'); + } + + return false; + }); + + /** + * Adds a deletion confirmation when removing a tag. + * + * @since 4.8.0 + * + * @return {void} + */ + $( '#edittag' ).on( 'click', '.delete', function( e ) { + if ( 'undefined' === typeof showNotice ) { + return true; + } + + // Confirms the deletion, a negative response means the deletion must not be executed. + var response = showNotice.warn(); + if ( ! response ) { + e.preventDefault(); + } + }); + + /** + * Adds an event handler to the form submit on the term overview page. + * + * Cancels default event handling and event bubbling. + * + * @since 2.8.0 + * + * @return {boolean} Always returns false to cancel the default event handling. + */ + $('#submit').on( 'click', function(){ + var form = $(this).parents('form'); + + if ( addingTerm ) { + // If we're adding a term, noop the button to avoid duplicate requests. + return false; + } + + addingTerm = true; + form.find( '.submit .spinner' ).addClass( 'is-active' ); + + /** + * Does a request to the server to add a new term to the database + * + * @param {string} r The response from the server. + * + * @return {void} + */ + $.post(ajaxurl, $('#addtag').serialize(), function(r){ + var res, parent, term, indent, i; + + addingTerm = false; + form.find( '.submit .spinner' ).removeClass( 'is-active' ); + + $('#ajax-response').empty(); + res = wpAjax.parseAjaxResponse( r, 'ajax-response' ); + + if ( res.errors && res.responses[0].errors[0].code === 'empty_term_name' ) { + validateForm( form ); + } + + if ( ! res || res.errors ) { + return; + } + + parent = form.find( 'select#parent' ).val(); + + // If the parent exists on this page, insert it below. Else insert it at the top of the list. + if ( parent > 0 && $('#tag-' + parent ).length > 0 ) { + // As the parent exists, insert the version with - - - prefixed. + $( '.tags #tag-' + parent ).after( res.responses[0].supplemental.noparents ); + } else { + // As the parent is not visible, insert the version with Parent - Child - ThisTerm. + $( '.tags' ).prepend( res.responses[0].supplemental.parents ); + } + + $('.tags .no-items').remove(); + + if ( form.find('select#parent') ) { + // Parents field exists, Add new term to the list. + term = res.responses[1].supplemental; + + // Create an indent for the Parent field. + indent = ''; + for ( i = 0; i < res.responses[1].position; i++ ) + indent += ' '; + + form.find( 'select#parent option:selected' ).after( '<option value="' + term.term_id + '">' + indent + term.name + '</option>' ); + } + + $('input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):visible, textarea:visible', form).val(''); + }); + + return false; + }); + +}); diff --git a/wp-admin/js/tags.min.js b/wp-admin/js/tags.min.js new file mode 100644 index 0000000..51510d1 --- /dev/null +++ b/wp-admin/js/tags.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(s){var o=!1;s("#the-list").on("click",".delete-tag",function(){var t,e=s(this),n=e.parents("tr"),a=!0;return(a="undefined"!=showNotice?showNotice.warn():a)&&(t=e.attr("href").replace(/[^?]*\?/,"").replace(/action=delete/,"action=delete-tag"),s.post(ajaxurl,t,function(e){"1"==e?(s("#ajax-response").empty(),n.fadeOut("normal",function(){n.remove()}),s('select#parent option[value="'+t.match(/tag_ID=(\d+)/)[1]+'"]').remove(),s("a.tag-link-"+t.match(/tag_ID=(\d+)/)[1]).remove()):("-1"==e?s("#ajax-response").empty().append('<div class="error"><p>'+wp.i18n.__("Sorry, you are not allowed to do that.")+"</p></div>"):s("#ajax-response").empty().append('<div class="error"><p>'+wp.i18n.__("Something went wrong.")+"</p></div>"),n.children().css("backgroundColor",""))}),n.children().css("backgroundColor","#f33")),!1}),s("#edittag").on("click",".delete",function(e){if("undefined"==typeof showNotice)return!0;showNotice.warn()||e.preventDefault()}),s("#submit").on("click",function(){var r=s(this).parents("form");return o||(o=!0,r.find(".submit .spinner").addClass("is-active"),s.post(ajaxurl,s("#addtag").serialize(),function(e){var t,n,a;if(o=!1,r.find(".submit .spinner").removeClass("is-active"),s("#ajax-response").empty(),(t=wpAjax.parseAjaxResponse(e,"ajax-response")).errors&&"empty_term_name"===t.responses[0].errors[0].code&&validateForm(r),t&&!t.errors){if(0<(e=r.find("select#parent").val())&&0<s("#tag-"+e).length?s(".tags #tag-"+e).after(t.responses[0].supplemental.noparents):s(".tags").prepend(t.responses[0].supplemental.parents),s(".tags .no-items").remove(),r.find("select#parent")){for(e=t.responses[1].supplemental,n="",a=0;a<t.responses[1].position;a++)n+=" ";r.find("select#parent option:selected").after('<option value="'+e.term_id+'">'+n+e.name+"</option>")}s('input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):visible, textarea:visible',r).val("")}})),!1})});
\ No newline at end of file diff --git a/wp-admin/js/theme-plugin-editor.js b/wp-admin/js/theme-plugin-editor.js new file mode 100644 index 0000000..8871b04 --- /dev/null +++ b/wp-admin/js/theme-plugin-editor.js @@ -0,0 +1,1026 @@ +/** + * @output wp-admin/js/theme-plugin-editor.js + */ + +/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1] }] */ + +if ( ! window.wp ) { + window.wp = {}; +} + +wp.themePluginEditor = (function( $ ) { + 'use strict'; + var component, TreeLinks, + __ = wp.i18n.__, _n = wp.i18n._n, sprintf = wp.i18n.sprintf; + + component = { + codeEditor: {}, + instance: null, + noticeElements: {}, + dirty: false, + lintErrors: [] + }; + + /** + * Initialize component. + * + * @since 4.9.0 + * + * @param {jQuery} form - Form element. + * @param {Object} settings - Settings. + * @param {Object|boolean} settings.codeEditor - Code editor settings (or `false` if syntax highlighting is disabled). + * @return {void} + */ + component.init = function init( form, settings ) { + + component.form = form; + if ( settings ) { + $.extend( component, settings ); + } + + component.noticeTemplate = wp.template( 'wp-file-editor-notice' ); + component.noticesContainer = component.form.find( '.editor-notices' ); + component.submitButton = component.form.find( ':input[name=submit]' ); + component.spinner = component.form.find( '.submit .spinner' ); + component.form.on( 'submit', component.submit ); + component.textarea = component.form.find( '#newcontent' ); + component.textarea.on( 'change', component.onChange ); + component.warning = $( '.file-editor-warning' ); + component.docsLookUpButton = component.form.find( '#docs-lookup' ); + component.docsLookUpList = component.form.find( '#docs-list' ); + + if ( component.warning.length > 0 ) { + component.showWarning(); + } + + if ( false !== component.codeEditor ) { + /* + * Defer adding notices until after DOM ready as workaround for WP Admin injecting + * its own managed dismiss buttons and also to prevent the editor from showing a notice + * when the file had linting errors to begin with. + */ + _.defer( function() { + component.initCodeEditor(); + } ); + } + + $( component.initFileBrowser ); + + $( window ).on( 'beforeunload', function() { + if ( component.dirty ) { + return __( 'The changes you made will be lost if you navigate away from this page.' ); + } + return undefined; + } ); + + component.docsLookUpList.on( 'change', function() { + var option = $( this ).val(); + if ( '' === option ) { + component.docsLookUpButton.prop( 'disabled', true ); + } else { + component.docsLookUpButton.prop( 'disabled', false ); + } + } ); + }; + + /** + * Set up and display the warning modal. + * + * @since 4.9.0 + * @return {void} + */ + component.showWarning = function() { + // Get the text within the modal. + var rawMessage = component.warning.find( '.file-editor-warning-message' ).text(); + // Hide all the #wpwrap content from assistive technologies. + $( '#wpwrap' ).attr( 'aria-hidden', 'true' ); + // Detach the warning modal from its position and append it to the body. + $( document.body ) + .addClass( 'modal-open' ) + .append( component.warning.detach() ); + // Reveal the modal and set focus on the go back button. + component.warning + .removeClass( 'hidden' ) + .find( '.file-editor-warning-go-back' ).trigger( 'focus' ); + // Get the links and buttons within the modal. + component.warningTabbables = component.warning.find( 'a, button' ); + // Attach event handlers. + component.warningTabbables.on( 'keydown', component.constrainTabbing ); + component.warning.on( 'click', '.file-editor-warning-dismiss', component.dismissWarning ); + // Make screen readers announce the warning message after a short delay (necessary for some screen readers). + setTimeout( function() { + wp.a11y.speak( wp.sanitize.stripTags( rawMessage.replace( /\s+/g, ' ' ) ), 'assertive' ); + }, 1000 ); + }; + + /** + * Constrain tabbing within the warning modal. + * + * @since 4.9.0 + * @param {Object} event jQuery event object. + * @return {void} + */ + component.constrainTabbing = function( event ) { + var firstTabbable, lastTabbable; + + if ( 9 !== event.which ) { + return; + } + + firstTabbable = component.warningTabbables.first()[0]; + lastTabbable = component.warningTabbables.last()[0]; + + if ( lastTabbable === event.target && ! event.shiftKey ) { + firstTabbable.focus(); + event.preventDefault(); + } else if ( firstTabbable === event.target && event.shiftKey ) { + lastTabbable.focus(); + event.preventDefault(); + } + }; + + /** + * Dismiss the warning modal. + * + * @since 4.9.0 + * @return {void} + */ + component.dismissWarning = function() { + + wp.ajax.post( 'dismiss-wp-pointer', { + pointer: component.themeOrPlugin + '_editor_notice' + }); + + // Hide modal. + component.warning.remove(); + $( '#wpwrap' ).removeAttr( 'aria-hidden' ); + $( 'body' ).removeClass( 'modal-open' ); + }; + + /** + * Callback for when a change happens. + * + * @since 4.9.0 + * @return {void} + */ + component.onChange = function() { + component.dirty = true; + component.removeNotice( 'file_saved' ); + }; + + /** + * Submit file via Ajax. + * + * @since 4.9.0 + * @param {jQuery.Event} event - Event. + * @return {void} + */ + component.submit = function( event ) { + var data = {}, request; + event.preventDefault(); // Prevent form submission in favor of Ajax below. + $.each( component.form.serializeArray(), function() { + data[ this.name ] = this.value; + } ); + + // Use value from codemirror if present. + if ( component.instance ) { + data.newcontent = component.instance.codemirror.getValue(); + } + + if ( component.isSaving ) { + return; + } + + // Scroll ot the line that has the error. + if ( component.lintErrors.length ) { + component.instance.codemirror.setCursor( component.lintErrors[0].from.line ); + return; + } + + component.isSaving = true; + component.textarea.prop( 'readonly', true ); + if ( component.instance ) { + component.instance.codemirror.setOption( 'readOnly', true ); + } + + component.spinner.addClass( 'is-active' ); + request = wp.ajax.post( 'edit-theme-plugin-file', data ); + + // Remove previous save notice before saving. + if ( component.lastSaveNoticeCode ) { + component.removeNotice( component.lastSaveNoticeCode ); + } + + request.done( function( response ) { + component.lastSaveNoticeCode = 'file_saved'; + component.addNotice({ + code: component.lastSaveNoticeCode, + type: 'success', + message: response.message, + dismissible: true + }); + component.dirty = false; + } ); + + request.fail( function( response ) { + var notice = $.extend( + { + code: 'save_error', + message: __( 'Something went wrong. Your change may not have been saved. Please try again. There is also a chance that you may need to manually fix and upload the file over FTP.' ) + }, + response, + { + type: 'error', + dismissible: true + } + ); + component.lastSaveNoticeCode = notice.code; + component.addNotice( notice ); + } ); + + request.always( function() { + component.spinner.removeClass( 'is-active' ); + component.isSaving = false; + + component.textarea.prop( 'readonly', false ); + if ( component.instance ) { + component.instance.codemirror.setOption( 'readOnly', false ); + } + } ); + }; + + /** + * Add notice. + * + * @since 4.9.0 + * + * @param {Object} notice - Notice. + * @param {string} notice.code - Code. + * @param {string} notice.type - Type. + * @param {string} notice.message - Message. + * @param {boolean} [notice.dismissible=false] - Dismissible. + * @param {Function} [notice.onDismiss] - Callback for when a user dismisses the notice. + * @return {jQuery} Notice element. + */ + component.addNotice = function( notice ) { + var noticeElement; + + if ( ! notice.code ) { + throw new Error( 'Missing code.' ); + } + + // Only let one notice of a given type be displayed at a time. + component.removeNotice( notice.code ); + + noticeElement = $( component.noticeTemplate( notice ) ); + noticeElement.hide(); + + noticeElement.find( '.notice-dismiss' ).on( 'click', function() { + component.removeNotice( notice.code ); + if ( notice.onDismiss ) { + notice.onDismiss( notice ); + } + } ); + + wp.a11y.speak( notice.message ); + + component.noticesContainer.append( noticeElement ); + noticeElement.slideDown( 'fast' ); + component.noticeElements[ notice.code ] = noticeElement; + return noticeElement; + }; + + /** + * Remove notice. + * + * @since 4.9.0 + * + * @param {string} code - Notice code. + * @return {boolean} Whether a notice was removed. + */ + component.removeNotice = function( code ) { + if ( component.noticeElements[ code ] ) { + component.noticeElements[ code ].slideUp( 'fast', function() { + $( this ).remove(); + } ); + delete component.noticeElements[ code ]; + return true; + } + return false; + }; + + /** + * Initialize code editor. + * + * @since 4.9.0 + * @return {void} + */ + component.initCodeEditor = function initCodeEditor() { + var codeEditorSettings, editor; + + codeEditorSettings = $.extend( {}, component.codeEditor ); + + /** + * Handle tabbing to the field before the editor. + * + * @since 4.9.0 + * + * @return {void} + */ + codeEditorSettings.onTabPrevious = function() { + $( '#templateside' ).find( ':tabbable' ).last().trigger( 'focus' ); + }; + + /** + * Handle tabbing to the field after the editor. + * + * @since 4.9.0 + * + * @return {void} + */ + codeEditorSettings.onTabNext = function() { + $( '#template' ).find( ':tabbable:not(.CodeMirror-code)' ).first().trigger( 'focus' ); + }; + + /** + * Handle change to the linting errors. + * + * @since 4.9.0 + * + * @param {Array} errors - List of linting errors. + * @return {void} + */ + codeEditorSettings.onChangeLintingErrors = function( errors ) { + component.lintErrors = errors; + + // Only disable the button in onUpdateErrorNotice when there are errors so users can still feel they can click the button. + if ( 0 === errors.length ) { + component.submitButton.toggleClass( 'disabled', false ); + } + }; + + /** + * Update error notice. + * + * @since 4.9.0 + * + * @param {Array} errorAnnotations - Error annotations. + * @return {void} + */ + codeEditorSettings.onUpdateErrorNotice = function onUpdateErrorNotice( errorAnnotations ) { + var noticeElement; + + component.submitButton.toggleClass( 'disabled', errorAnnotations.length > 0 ); + + if ( 0 !== errorAnnotations.length ) { + noticeElement = component.addNotice({ + code: 'lint_errors', + type: 'error', + message: sprintf( + /* translators: %s: Error count. */ + _n( + 'There is %s error which must be fixed before you can update this file.', + 'There are %s errors which must be fixed before you can update this file.', + errorAnnotations.length + ), + String( errorAnnotations.length ) + ), + dismissible: false + }); + noticeElement.find( 'input[type=checkbox]' ).on( 'click', function() { + codeEditorSettings.onChangeLintingErrors( [] ); + component.removeNotice( 'lint_errors' ); + } ); + } else { + component.removeNotice( 'lint_errors' ); + } + }; + + editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings ); + editor.codemirror.on( 'change', component.onChange ); + + // Improve the editor accessibility. + $( editor.codemirror.display.lineDiv ) + .attr({ + role: 'textbox', + 'aria-multiline': 'true', + 'aria-labelledby': 'theme-plugin-editor-label', + 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4' + }); + + // Focus the editor when clicking on its label. + $( '#theme-plugin-editor-label' ).on( 'click', function() { + editor.codemirror.focus(); + }); + + component.instance = editor; + }; + + /** + * Initialization of the file browser's folder states. + * + * @since 4.9.0 + * @return {void} + */ + component.initFileBrowser = function initFileBrowser() { + + var $templateside = $( '#templateside' ); + + // Collapse all folders. + $templateside.find( '[role="group"]' ).parent().attr( 'aria-expanded', false ); + + // Expand ancestors to the current file. + $templateside.find( '.notice' ).parents( '[aria-expanded]' ).attr( 'aria-expanded', true ); + + // Find Tree elements and enhance them. + $templateside.find( '[role="tree"]' ).each( function() { + var treeLinks = new TreeLinks( this ); + treeLinks.init(); + } ); + + // Scroll the current file into view. + $templateside.find( '.current-file:first' ).each( function() { + if ( this.scrollIntoViewIfNeeded ) { + this.scrollIntoViewIfNeeded(); + } else { + this.scrollIntoView( false ); + } + } ); + }; + + /* jshint ignore:start */ + /* jscs:disable */ + /* eslint-disable */ + + /** + * Creates a new TreeitemLink. + * + * @since 4.9.0 + * @class + * @private + * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2b.html|W3C Treeview Example} + * @license W3C-20150513 + */ + var TreeitemLink = (function () { + /** + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: TreeitemLink.js + * + * Desc: Treeitem widget that implements ARIA Authoring Practices + * for a tree being used as a file viewer + * + * Author: Jon Gunderson, Ku Ja Eun and Nicholas Hoyt + */ + + /** + * @constructor + * + * @desc + * Treeitem object for representing the state and user interactions for a + * treeItem widget + * + * @param node + * An element with the role=tree attribute + */ + + var TreeitemLink = function (node, treeObj, group) { + + // Check whether node is a DOM element. + if (typeof node !== 'object') { + return; + } + + node.tabIndex = -1; + this.tree = treeObj; + this.groupTreeitem = group; + this.domNode = node; + this.label = node.textContent.trim(); + this.stopDefaultClick = false; + + if (node.getAttribute('aria-label')) { + this.label = node.getAttribute('aria-label').trim(); + } + + this.isExpandable = false; + this.isVisible = false; + this.inGroup = false; + + if (group) { + this.inGroup = true; + } + + var elem = node.firstElementChild; + + while (elem) { + + if (elem.tagName.toLowerCase() == 'ul') { + elem.setAttribute('role', 'group'); + this.isExpandable = true; + break; + } + + elem = elem.nextElementSibling; + } + + this.keyCode = Object.freeze({ + RETURN: 13, + SPACE: 32, + PAGEUP: 33, + PAGEDOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40 + }); + }; + + TreeitemLink.prototype.init = function () { + this.domNode.tabIndex = -1; + + if (!this.domNode.getAttribute('role')) { + this.domNode.setAttribute('role', 'treeitem'); + } + + this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); + this.domNode.addEventListener('click', this.handleClick.bind(this)); + this.domNode.addEventListener('focus', this.handleFocus.bind(this)); + this.domNode.addEventListener('blur', this.handleBlur.bind(this)); + + if (this.isExpandable) { + this.domNode.firstElementChild.addEventListener('mouseover', this.handleMouseOver.bind(this)); + this.domNode.firstElementChild.addEventListener('mouseout', this.handleMouseOut.bind(this)); + } + else { + this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this)); + this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this)); + } + }; + + TreeitemLink.prototype.isExpanded = function () { + + if (this.isExpandable) { + return this.domNode.getAttribute('aria-expanded') === 'true'; + } + + return false; + + }; + + /* EVENT HANDLERS */ + + TreeitemLink.prototype.handleKeydown = function (event) { + var tgt = event.currentTarget, + flag = false, + _char = event.key, + clickEvent; + + function isPrintableCharacter(str) { + return str.length === 1 && str.match(/\S/); + } + + function printableCharacter(item) { + if (_char == '*') { + item.tree.expandAllSiblingItems(item); + flag = true; + } + else { + if (isPrintableCharacter(_char)) { + item.tree.setFocusByFirstCharacter(item, _char); + flag = true; + } + } + } + + this.stopDefaultClick = false; + + if (event.altKey || event.ctrlKey || event.metaKey) { + return; + } + + if (event.shift) { + if (event.keyCode == this.keyCode.SPACE || event.keyCode == this.keyCode.RETURN) { + event.stopPropagation(); + this.stopDefaultClick = true; + } + else { + if (isPrintableCharacter(_char)) { + printableCharacter(this); + } + } + } + else { + switch (event.keyCode) { + case this.keyCode.SPACE: + case this.keyCode.RETURN: + if (this.isExpandable) { + if (this.isExpanded()) { + this.tree.collapseTreeitem(this); + } + else { + this.tree.expandTreeitem(this); + } + flag = true; + } + else { + event.stopPropagation(); + this.stopDefaultClick = true; + } + break; + + case this.keyCode.UP: + this.tree.setFocusToPreviousItem(this); + flag = true; + break; + + case this.keyCode.DOWN: + this.tree.setFocusToNextItem(this); + flag = true; + break; + + case this.keyCode.RIGHT: + if (this.isExpandable) { + if (this.isExpanded()) { + this.tree.setFocusToNextItem(this); + } + else { + this.tree.expandTreeitem(this); + } + } + flag = true; + break; + + case this.keyCode.LEFT: + if (this.isExpandable && this.isExpanded()) { + this.tree.collapseTreeitem(this); + flag = true; + } + else { + if (this.inGroup) { + this.tree.setFocusToParentItem(this); + flag = true; + } + } + break; + + case this.keyCode.HOME: + this.tree.setFocusToFirstItem(); + flag = true; + break; + + case this.keyCode.END: + this.tree.setFocusToLastItem(); + flag = true; + break; + + default: + if (isPrintableCharacter(_char)) { + printableCharacter(this); + } + break; + } + } + + if (flag) { + event.stopPropagation(); + event.preventDefault(); + } + }; + + TreeitemLink.prototype.handleClick = function (event) { + + // Only process click events that directly happened on this treeitem. + if (event.target !== this.domNode && event.target !== this.domNode.firstElementChild) { + return; + } + + if (this.isExpandable) { + if (this.isExpanded()) { + this.tree.collapseTreeitem(this); + } + else { + this.tree.expandTreeitem(this); + } + event.stopPropagation(); + } + }; + + TreeitemLink.prototype.handleFocus = function (event) { + var node = this.domNode; + if (this.isExpandable) { + node = node.firstElementChild; + } + node.classList.add('focus'); + }; + + TreeitemLink.prototype.handleBlur = function (event) { + var node = this.domNode; + if (this.isExpandable) { + node = node.firstElementChild; + } + node.classList.remove('focus'); + }; + + TreeitemLink.prototype.handleMouseOver = function (event) { + event.currentTarget.classList.add('hover'); + }; + + TreeitemLink.prototype.handleMouseOut = function (event) { + event.currentTarget.classList.remove('hover'); + }; + + return TreeitemLink; + })(); + + /** + * Creates a new TreeLinks. + * + * @since 4.9.0 + * @class + * @private + * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2b.html|W3C Treeview Example} + * @license W3C-20150513 + */ + TreeLinks = (function () { + /* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: TreeLinks.js + * + * Desc: Tree widget that implements ARIA Authoring Practices + * for a tree being used as a file viewer + * + * Author: Jon Gunderson, Ku Ja Eun and Nicholas Hoyt + */ + + /* + * @constructor + * + * @desc + * Tree item object for representing the state and user interactions for a + * tree widget + * + * @param node + * An element with the role=tree attribute + */ + + var TreeLinks = function (node) { + // Check whether node is a DOM element. + if (typeof node !== 'object') { + return; + } + + this.domNode = node; + + this.treeitems = []; + this.firstChars = []; + + this.firstTreeitem = null; + this.lastTreeitem = null; + + }; + + TreeLinks.prototype.init = function () { + + function findTreeitems(node, tree, group) { + + var elem = node.firstElementChild; + var ti = group; + + while (elem) { + + if ((elem.tagName.toLowerCase() === 'li' && elem.firstElementChild.tagName.toLowerCase() === 'span') || elem.tagName.toLowerCase() === 'a') { + ti = new TreeitemLink(elem, tree, group); + ti.init(); + tree.treeitems.push(ti); + tree.firstChars.push(ti.label.substring(0, 1).toLowerCase()); + } + + if (elem.firstElementChild) { + findTreeitems(elem, tree, ti); + } + + elem = elem.nextElementSibling; + } + } + + // Initialize pop up menus. + if (!this.domNode.getAttribute('role')) { + this.domNode.setAttribute('role', 'tree'); + } + + findTreeitems(this.domNode, this, false); + + this.updateVisibleTreeitems(); + + this.firstTreeitem.domNode.tabIndex = 0; + + }; + + TreeLinks.prototype.setFocusToItem = function (treeitem) { + + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + + if (ti === treeitem) { + ti.domNode.tabIndex = 0; + ti.domNode.focus(); + } + else { + ti.domNode.tabIndex = -1; + } + } + + }; + + TreeLinks.prototype.setFocusToNextItem = function (currentItem) { + + var nextItem = false; + + for (var i = (this.treeitems.length - 1); i >= 0; i--) { + var ti = this.treeitems[i]; + if (ti === currentItem) { + break; + } + if (ti.isVisible) { + nextItem = ti; + } + } + + if (nextItem) { + this.setFocusToItem(nextItem); + } + + }; + + TreeLinks.prototype.setFocusToPreviousItem = function (currentItem) { + + var prevItem = false; + + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + if (ti === currentItem) { + break; + } + if (ti.isVisible) { + prevItem = ti; + } + } + + if (prevItem) { + this.setFocusToItem(prevItem); + } + }; + + TreeLinks.prototype.setFocusToParentItem = function (currentItem) { + + if (currentItem.groupTreeitem) { + this.setFocusToItem(currentItem.groupTreeitem); + } + }; + + TreeLinks.prototype.setFocusToFirstItem = function () { + this.setFocusToItem(this.firstTreeitem); + }; + + TreeLinks.prototype.setFocusToLastItem = function () { + this.setFocusToItem(this.lastTreeitem); + }; + + TreeLinks.prototype.expandTreeitem = function (currentItem) { + + if (currentItem.isExpandable) { + currentItem.domNode.setAttribute('aria-expanded', true); + this.updateVisibleTreeitems(); + } + + }; + + TreeLinks.prototype.expandAllSiblingItems = function (currentItem) { + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + + if ((ti.groupTreeitem === currentItem.groupTreeitem) && ti.isExpandable) { + this.expandTreeitem(ti); + } + } + + }; + + TreeLinks.prototype.collapseTreeitem = function (currentItem) { + + var groupTreeitem = false; + + if (currentItem.isExpanded()) { + groupTreeitem = currentItem; + } + else { + groupTreeitem = currentItem.groupTreeitem; + } + + if (groupTreeitem) { + groupTreeitem.domNode.setAttribute('aria-expanded', false); + this.updateVisibleTreeitems(); + this.setFocusToItem(groupTreeitem); + } + + }; + + TreeLinks.prototype.updateVisibleTreeitems = function () { + + this.firstTreeitem = this.treeitems[0]; + + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + + var parent = ti.domNode.parentNode; + + ti.isVisible = true; + + while (parent && (parent !== this.domNode)) { + + if (parent.getAttribute('aria-expanded') == 'false') { + ti.isVisible = false; + } + parent = parent.parentNode; + } + + if (ti.isVisible) { + this.lastTreeitem = ti; + } + } + + }; + + TreeLinks.prototype.setFocusByFirstCharacter = function (currentItem, _char) { + var start, index; + _char = _char.toLowerCase(); + + // Get start index for search based on position of currentItem. + start = this.treeitems.indexOf(currentItem) + 1; + if (start === this.treeitems.length) { + start = 0; + } + + // Check remaining slots in the menu. + index = this.getIndexFirstChars(start, _char); + + // If not found in remaining slots, check from beginning. + if (index === -1) { + index = this.getIndexFirstChars(0, _char); + } + + // If match was found... + if (index > -1) { + this.setFocusToItem(this.treeitems[index]); + } + }; + + TreeLinks.prototype.getIndexFirstChars = function (startIndex, _char) { + for (var i = startIndex; i < this.firstChars.length; i++) { + if (this.treeitems[i].isVisible) { + if (_char === this.firstChars[i]) { + return i; + } + } + } + return -1; + }; + + return TreeLinks; + })(); + + /* jshint ignore:end */ + /* jscs:enable */ + /* eslint-enable */ + + return component; +})( jQuery ); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 4.9.0 + * @deprecated 5.5.0 + * + * @type {object} + */ +wp.themePluginEditor.l10n = wp.themePluginEditor.l10n || { + saveAlert: '', + saveError: '', + lintError: { + alternative: 'wp.i18n', + func: function() { + return { + singular: '', + plural: '' + }; + } + } +}; + +wp.themePluginEditor.l10n = window.wp.deprecateL10nObject( 'wp.themePluginEditor.l10n', wp.themePluginEditor.l10n, '5.5.0' ); diff --git a/wp-admin/js/theme-plugin-editor.min.js b/wp-admin/js/theme-plugin-editor.min.js new file mode 100644 index 0000000..a760ad1 --- /dev/null +++ b/wp-admin/js/theme-plugin-editor.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp||(window.wp={}),wp.themePluginEditor=function(i){"use strict";var t,o=wp.i18n.__,s=wp.i18n._n,n=wp.i18n.sprintf,r={codeEditor:{},instance:null,noticeElements:{},dirty:!1,lintErrors:[],init:function(e,t){r.form=e,t&&i.extend(r,t),r.noticeTemplate=wp.template("wp-file-editor-notice"),r.noticesContainer=r.form.find(".editor-notices"),r.submitButton=r.form.find(":input[name=submit]"),r.spinner=r.form.find(".submit .spinner"),r.form.on("submit",r.submit),r.textarea=r.form.find("#newcontent"),r.textarea.on("change",r.onChange),r.warning=i(".file-editor-warning"),r.docsLookUpButton=r.form.find("#docs-lookup"),r.docsLookUpList=r.form.find("#docs-list"),0<r.warning.length&&r.showWarning(),!1!==r.codeEditor&&_.defer(function(){r.initCodeEditor()}),i(r.initFileBrowser),i(window).on("beforeunload",function(){if(r.dirty)return o("The changes you made will be lost if you navigate away from this page.")}),r.docsLookUpList.on("change",function(){""===i(this).val()?r.docsLookUpButton.prop("disabled",!0):r.docsLookUpButton.prop("disabled",!1)})},showWarning:function(){var e=r.warning.find(".file-editor-warning-message").text();i("#wpwrap").attr("aria-hidden","true"),i(document.body).addClass("modal-open").append(r.warning.detach()),r.warning.removeClass("hidden").find(".file-editor-warning-go-back").trigger("focus"),r.warningTabbables=r.warning.find("a, button"),r.warningTabbables.on("keydown",r.constrainTabbing),r.warning.on("click",".file-editor-warning-dismiss",r.dismissWarning),setTimeout(function(){wp.a11y.speak(wp.sanitize.stripTags(e.replace(/\s+/g," ")),"assertive")},1e3)},constrainTabbing:function(e){var t,i;9===e.which&&(t=r.warningTabbables.first()[0],(i=r.warningTabbables.last()[0])!==e.target||e.shiftKey?t===e.target&&e.shiftKey&&(i.focus(),e.preventDefault()):(t.focus(),e.preventDefault()))},dismissWarning:function(){wp.ajax.post("dismiss-wp-pointer",{pointer:r.themeOrPlugin+"_editor_notice"}),r.warning.remove(),i("#wpwrap").removeAttr("aria-hidden"),i("body").removeClass("modal-open")},onChange:function(){r.dirty=!0,r.removeNotice("file_saved")},submit:function(e){var t={};e.preventDefault(),i.each(r.form.serializeArray(),function(){t[this.name]=this.value}),r.instance&&(t.newcontent=r.instance.codemirror.getValue()),r.isSaving||(r.lintErrors.length?r.instance.codemirror.setCursor(r.lintErrors[0].from.line):(r.isSaving=!0,r.textarea.prop("readonly",!0),r.instance&&r.instance.codemirror.setOption("readOnly",!0),r.spinner.addClass("is-active"),e=wp.ajax.post("edit-theme-plugin-file",t),r.lastSaveNoticeCode&&r.removeNotice(r.lastSaveNoticeCode),e.done(function(e){r.lastSaveNoticeCode="file_saved",r.addNotice({code:r.lastSaveNoticeCode,type:"success",message:e.message,dismissible:!0}),r.dirty=!1}),e.fail(function(e){e=i.extend({code:"save_error",message:o("Something went wrong. Your change may not have been saved. Please try again. There is also a chance that you may need to manually fix and upload the file over FTP.")},e,{type:"error",dismissible:!0});r.lastSaveNoticeCode=e.code,r.addNotice(e)}),e.always(function(){r.spinner.removeClass("is-active"),r.isSaving=!1,r.textarea.prop("readonly",!1),r.instance&&r.instance.codemirror.setOption("readOnly",!1)})))},addNotice:function(e){var t;if(e.code)return r.removeNotice(e.code),(t=i(r.noticeTemplate(e))).hide(),t.find(".notice-dismiss").on("click",function(){r.removeNotice(e.code),e.onDismiss&&e.onDismiss(e)}),wp.a11y.speak(e.message),r.noticesContainer.append(t),t.slideDown("fast"),r.noticeElements[e.code]=t;throw new Error("Missing code.")},removeNotice:function(e){return!!r.noticeElements[e]&&(r.noticeElements[e].slideUp("fast",function(){i(this).remove()}),delete r.noticeElements[e],!0)},initCodeEditor:function(){var e,t=i.extend({},r.codeEditor);t.onTabPrevious=function(){i("#templateside").find(":tabbable").last().trigger("focus")},t.onTabNext=function(){i("#template").find(":tabbable:not(.CodeMirror-code)").first().trigger("focus")},t.onChangeLintingErrors=function(e){0===(r.lintErrors=e).length&&r.submitButton.toggleClass("disabled",!1)},t.onUpdateErrorNotice=function(e){r.submitButton.toggleClass("disabled",0<e.length),0!==e.length?r.addNotice({code:"lint_errors",type:"error",message:n(s("There is %s error which must be fixed before you can update this file.","There are %s errors which must be fixed before you can update this file.",e.length),String(e.length)),dismissible:!1}).find("input[type=checkbox]").on("click",function(){t.onChangeLintingErrors([]),r.removeNotice("lint_errors")}):r.removeNotice("lint_errors")},(e=wp.codeEditor.initialize(i("#newcontent"),t)).codemirror.on("change",r.onChange),i(e.codemirror.display.lineDiv).attr({role:"textbox","aria-multiline":"true","aria-labelledby":"theme-plugin-editor-label","aria-describedby":"editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"}),i("#theme-plugin-editor-label").on("click",function(){e.codemirror.focus()}),r.instance=e},initFileBrowser:function(){var e=i("#templateside");e.find('[role="group"]').parent().attr("aria-expanded",!1),e.find(".notice").parents("[aria-expanded]").attr("aria-expanded",!0),e.find('[role="tree"]').each(function(){new t(this).init()}),e.find(".current-file:first").each(function(){this.scrollIntoViewIfNeeded?this.scrollIntoViewIfNeeded():this.scrollIntoView(!1)})}},a=(e.prototype.init=function(){this.domNode.tabIndex=-1,this.domNode.getAttribute("role")||this.domNode.setAttribute("role","treeitem"),this.domNode.addEventListener("keydown",this.handleKeydown.bind(this)),this.domNode.addEventListener("click",this.handleClick.bind(this)),this.domNode.addEventListener("focus",this.handleFocus.bind(this)),this.domNode.addEventListener("blur",this.handleBlur.bind(this)),(this.isExpandable?(this.domNode.firstElementChild.addEventListener("mouseover",this.handleMouseOver.bind(this)),this.domNode.firstElementChild):(this.domNode.addEventListener("mouseover",this.handleMouseOver.bind(this)),this.domNode)).addEventListener("mouseout",this.handleMouseOut.bind(this))},e.prototype.isExpanded=function(){return!!this.isExpandable&&"true"===this.domNode.getAttribute("aria-expanded")},e.prototype.handleKeydown=function(e){e.currentTarget;var t=!1,i=e.key;function o(e){return 1===e.length&&e.match(/\S/)}function s(e){"*"==i?(e.tree.expandAllSiblingItems(e),t=!0):o(i)&&(e.tree.setFocusByFirstCharacter(e,i),t=!0)}if(this.stopDefaultClick=!1,!(e.altKey||e.ctrlKey||e.metaKey)){if(e.shift)e.keyCode==this.keyCode.SPACE||e.keyCode==this.keyCode.RETURN?(e.stopPropagation(),this.stopDefaultClick=!0):o(i)&&s(this);else switch(e.keyCode){case this.keyCode.SPACE:case this.keyCode.RETURN:this.isExpandable?(this.isExpanded()?this.tree.collapseTreeitem(this):this.tree.expandTreeitem(this),t=!0):(e.stopPropagation(),this.stopDefaultClick=!0);break;case this.keyCode.UP:this.tree.setFocusToPreviousItem(this),t=!0;break;case this.keyCode.DOWN:this.tree.setFocusToNextItem(this),t=!0;break;case this.keyCode.RIGHT:this.isExpandable&&(this.isExpanded()?this.tree.setFocusToNextItem(this):this.tree.expandTreeitem(this)),t=!0;break;case this.keyCode.LEFT:this.isExpandable&&this.isExpanded()?(this.tree.collapseTreeitem(this),t=!0):this.inGroup&&(this.tree.setFocusToParentItem(this),t=!0);break;case this.keyCode.HOME:this.tree.setFocusToFirstItem(),t=!0;break;case this.keyCode.END:this.tree.setFocusToLastItem(),t=!0;break;default:o(i)&&s(this)}t&&(e.stopPropagation(),e.preventDefault())}},e.prototype.handleClick=function(e){e.target!==this.domNode&&e.target!==this.domNode.firstElementChild||this.isExpandable&&(this.isExpanded()?this.tree.collapseTreeitem(this):this.tree.expandTreeitem(this),e.stopPropagation())},e.prototype.handleFocus=function(e){var t=this.domNode;(t=this.isExpandable?t.firstElementChild:t).classList.add("focus")},e.prototype.handleBlur=function(e){var t=this.domNode;(t=this.isExpandable?t.firstElementChild:t).classList.remove("focus")},e.prototype.handleMouseOver=function(e){e.currentTarget.classList.add("hover")},e.prototype.handleMouseOut=function(e){e.currentTarget.classList.remove("hover")},e);function e(e,t,i){if("object"==typeof e){e.tabIndex=-1,this.tree=t,this.groupTreeitem=i,this.domNode=e,this.label=e.textContent.trim(),this.stopDefaultClick=!1,e.getAttribute("aria-label")&&(this.label=e.getAttribute("aria-label").trim()),this.isExpandable=!1,this.isVisible=!1,this.inGroup=!1,i&&(this.inGroup=!0);for(var o=e.firstElementChild;o;){if("ul"==o.tagName.toLowerCase()){o.setAttribute("role","group"),this.isExpandable=!0;break}o=o.nextElementSibling}this.keyCode=Object.freeze({RETURN:13,SPACE:32,PAGEUP:33,PAGEDOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40})}}function d(e){"object"==typeof e&&(this.domNode=e,this.treeitems=[],this.firstChars=[],this.firstTreeitem=null,this.lastTreeitem=null)}return d.prototype.init=function(){this.domNode.getAttribute("role")||this.domNode.setAttribute("role","tree"),function e(t,i,o){for(var s=t.firstElementChild,n=o;s;)("li"===s.tagName.toLowerCase()&&"span"===s.firstElementChild.tagName.toLowerCase()||"a"===s.tagName.toLowerCase())&&((n=new a(s,i,o)).init(),i.treeitems.push(n),i.firstChars.push(n.label.substring(0,1).toLowerCase())),s.firstElementChild&&e(s,i,n),s=s.nextElementSibling}(this.domNode,this,!1),this.updateVisibleTreeitems(),this.firstTreeitem.domNode.tabIndex=0},d.prototype.setFocusToItem=function(e){for(var t=0;t<this.treeitems.length;t++){var i=this.treeitems[t];i===e?(i.domNode.tabIndex=0,i.domNode.focus()):i.domNode.tabIndex=-1}},d.prototype.setFocusToNextItem=function(e){for(var t=!1,i=this.treeitems.length-1;0<=i;i--){var o=this.treeitems[i];if(o===e)break;o.isVisible&&(t=o)}t&&this.setFocusToItem(t)},d.prototype.setFocusToPreviousItem=function(e){for(var t=!1,i=0;i<this.treeitems.length;i++){var o=this.treeitems[i];if(o===e)break;o.isVisible&&(t=o)}t&&this.setFocusToItem(t)},d.prototype.setFocusToParentItem=function(e){e.groupTreeitem&&this.setFocusToItem(e.groupTreeitem)},d.prototype.setFocusToFirstItem=function(){this.setFocusToItem(this.firstTreeitem)},d.prototype.setFocusToLastItem=function(){this.setFocusToItem(this.lastTreeitem)},d.prototype.expandTreeitem=function(e){e.isExpandable&&(e.domNode.setAttribute("aria-expanded",!0),this.updateVisibleTreeitems())},d.prototype.expandAllSiblingItems=function(e){for(var t=0;t<this.treeitems.length;t++){var i=this.treeitems[t];i.groupTreeitem===e.groupTreeitem&&i.isExpandable&&this.expandTreeitem(i)}},d.prototype.collapseTreeitem=function(e){var t=!1;(t=e.isExpanded()?e:e.groupTreeitem)&&(t.domNode.setAttribute("aria-expanded",!1),this.updateVisibleTreeitems(),this.setFocusToItem(t))},d.prototype.updateVisibleTreeitems=function(){this.firstTreeitem=this.treeitems[0];for(var e=0;e<this.treeitems.length;e++){var t=this.treeitems[e],i=t.domNode.parentNode;for(t.isVisible=!0;i&&i!==this.domNode;)"false"==i.getAttribute("aria-expanded")&&(t.isVisible=!1),i=i.parentNode;t.isVisible&&(this.lastTreeitem=t)}},d.prototype.setFocusByFirstCharacter=function(e,t){t=t.toLowerCase(),(e=this.treeitems.indexOf(e)+1)===this.treeitems.length&&(e=0),-1<(e=-1===(e=this.getIndexFirstChars(e,t))?this.getIndexFirstChars(0,t):e)&&this.setFocusToItem(this.treeitems[e])},d.prototype.getIndexFirstChars=function(e,t){for(var i=e;i<this.firstChars.length;i++)if(this.treeitems[i].isVisible&&t===this.firstChars[i])return i;return-1},t=d,r}(jQuery),wp.themePluginEditor.l10n=wp.themePluginEditor.l10n||{saveAlert:"",saveError:"",lintError:{alternative:"wp.i18n",func:function(){return{singular:"",plural:""}}}},wp.themePluginEditor.l10n=window.wp.deprecateL10nObject("wp.themePluginEditor.l10n",wp.themePluginEditor.l10n,"5.5.0");
\ No newline at end of file diff --git a/wp-admin/js/theme.js b/wp-admin/js/theme.js new file mode 100644 index 0000000..5daf04f --- /dev/null +++ b/wp-admin/js/theme.js @@ -0,0 +1,2120 @@ +/** + * @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 + .append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) ) + .append( view.el ) + .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.tooglePreviewDeviceButtons( 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.tooglePreviewDeviceButtons( device ); + }, + + tooglePreviewDeviceButtons: 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( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' ); + } + + 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 ); + + // Sanity 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: { + placeholder: l10n.searchPlaceholder, + 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( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' ); + $( '.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( '<span class="tag">' + name + '</span>' ); + }); + + // 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(); }); +}); diff --git a/wp-admin/js/theme.min.js b/wp-admin/js/theme.min.js new file mode 100644 index 0000000..8f26e38 --- /dev/null +++ b/wp-admin/js/theme.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +window.wp=window.wp||{},function(n){var o,a;function e(e,t){Backbone.history._hasPushState&&Backbone.Router.prototype.navigate.call(this,e,t)}(o=wp.themes=wp.themes||{}).data=_wpThemeSettings,a=o.data.l10n,o.isInstall=!!o.data.settings.isInstall,_.extend(o,{model:{},view:{},routes:{},router:{},template:wp.template}),o.Model=Backbone.Model.extend({initialize:function(){var e;this.get("slug")&&(-1!==_.indexOf(o.data.installedThemes,this.get("slug"))&&this.set({installed:!0}),o.data.activeTheme===this.get("slug"))&&this.set({active:!0}),this.set({id:this.get("slug")||this.get("id")}),this.has("sections")&&(e=this.get("sections").description,this.set({description:e}))}}),o.view.Appearance=wp.Backbone.View.extend({el:"#wpbody-content .wrap .theme-browser",window:n(window),page:0,initialize:function(e){_.bindAll(this,"scroller"),this.SearchView=e.SearchView||o.view.Search,this.window.on("scroll",_.throttle(this.scroller,300))},render:function(){this.view=new o.view.Themes({collection:this.collection,parent:this}),this.search(),this.$el.removeClass("search-loading"),this.view.render(),this.$el.empty().append(this.view.el).addClass("rendered")},searchContainer:n(".search-form"),search:function(){var e;1!==o.data.themes.length&&(e=new this.SearchView({collection:this.collection,parent:this}),(this.SearchView=e).render(),this.searchContainer.append(n.parseHTML('<label class="screen-reader-text" for="wp-filter-search-input">'+a.search+"</label>")).append(e.el).on("submit",function(e){e.preventDefault()}))},scroller:function(){var e=this,t=this.window.scrollTop()+e.window.height(),e=e.$el.offset().top+e.$el.outerHeight(!1)-e.window.height();Math.round(.9*e)<t&&this.trigger("theme:scroll")}}),o.Collection=Backbone.Collection.extend({model:o.Model,terms:"",doSearch:function(e){this.terms!==e&&(this.terms=e,0<this.terms.length&&this.search(this.terms),""===this.terms&&(this.reset(o.data.themes),n("body").removeClass("no-results")),this.trigger("themes:update"))},search:function(t){var i,e,s,r,a;this.reset(o.data.themes,{silent:!0}),t=(t=(t=t.trim()).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")).replace(/ /g,")(?=.*"),i=new RegExp("^(?=.*"+t+").+","i"),0===(e=this.filter(function(e){return s=e.get("name").replace(/(<([^>]+)>)/gi,""),r=e.get("description").replace(/(<([^>]+)>)/gi,""),a=e.get("author").replace(/(<([^>]+)>)/gi,""),s=_.union([s,e.get("id"),r,a,e.get("tags")]),i.test(e.get("author"))&&2<t.length&&e.set("displayAuthor",!0),i.test(s)})).length?this.trigger("query:empty"):n("body").removeClass("no-results"),this.reset(e)},paginate:function(e){var t=this;return e=e||0,t=_(t.rest(20*e)),t=_(t.first(20))},count:!1,query:function(t){var e,i,s,r=this.queries,a=this;if(this.currentQuery.request=t,e=_.find(r,function(e){return _.isEqual(e.request,t)}),(i=_.has(t,"page"))||(this.currentQuery.page=1),e||i){if(i)return this.apiCall(t,i).done(function(e){a.add(e.themes),a.trigger("query:success"),a.loadingThemes=!1}).fail(function(){a.trigger("query:fail")});0===e.themes.length?a.trigger("query:empty"):n("body").removeClass("no-results"),_.isNumber(e.total)&&(this.count=e.total),this.reset(e.themes),e.total||(this.count=this.length),this.trigger("themes:update"),this.trigger("query:success",this.count)}else this.apiCall(t).done(function(e){e.themes&&(a.reset(e.themes),s=e.info.results,r.push({themes:e.themes,request:t,total:s})),a.trigger("themes:update"),a.trigger("query:success",s),e.themes&&0===e.themes.length&&a.trigger("query:empty")}).fail(function(){a.trigger("query:fail")})},queries:[],currentQuery:{page:1,request:{}},apiCall:function(e,t){return wp.ajax.send("query-themes",{data:{request:_.extend({per_page:100},e)},beforeSend:function(){t||n("body").addClass("loading-content").removeClass("no-results")}})},loadingThemes:!1}),o.view.Theme=wp.Backbone.View.extend({className:"theme",state:"grid",html:o.template("theme"),events:{click:o.isInstall?"preview":"expand",keydown:o.isInstall?"preview":"expand",touchend:o.isInstall?"preview":"expand",keyup:"addFocus",touchmove:"preventExpand","click .theme-install":"installTheme","click .update-message":"updateTheme"},touchDrag:!1,initialize:function(){this.model.on("change",this.render,this)},render:function(){var e=this.model.toJSON();this.$el.html(this.html(e)).attr("data-slug",e.id),this.activeTheme(),this.model.get("displayAuthor")&&this.$el.addClass("display-author")},activeTheme:function(){this.model.get("active")&&this.$el.addClass("active")},addFocus:function(){var e=n(":focus").hasClass("theme")?n(":focus"):n(":focus").parents(".theme");n(".theme.focus").removeClass("focus"),e.addClass("focus")},expand:function(e){if("keydown"!==(e=e||window.event).type||13===e.which||32===e.which)return!0===this.touchDrag?this.touchDrag=!1:void(n(e.target).is(".theme-actions a")||n(e.target).is(".theme-actions a, .update-message, .button-link, .notice-dismiss")||(o.focusedTheme=this.$el,this.trigger("theme:expand",this.model.cid)))},preventExpand:function(){this.touchDrag=!0},preview:function(e){var t,i,s=this;if(e=e||window.event,!0===this.touchDrag)return this.touchDrag=!1;n(e.target).not(".install-theme-preview").parents(".theme-actions").length||"keydown"===e.type&&13!==e.which&&32!==e.which||"keydown"===e.type&&13!==e.which&&n(":focus").hasClass("button")||(e.preventDefault(),e=e||window.event,o.focusedTheme=this.$el,o.preview=i=new o.view.Preview({model:this.model}),i.render(),this.setNavButtonsState(),1===this.model.collection.length?i.$el.addClass("no-navigation"):i.$el.removeClass("no-navigation"),n("div.wrap").append(i.el),this.listenTo(i,"theme:next",function(){if(t=s.model,_.isUndefined(s.current)||(t=s.current),s.current=s.model.collection.at(s.model.collection.indexOf(t)+1),_.isUndefined(s.current))return s.options.parent.parent.trigger("theme:end"),s.current=t;i.model=s.current,i.render(),this.setNavButtonsState(),n(".next-theme").trigger("focus")}).listenTo(i,"theme:previous",function(){t=s.model,0===s.model.collection.indexOf(s.current)||(_.isUndefined(s.current)||(t=s.current),s.current=s.model.collection.at(s.model.collection.indexOf(t)-1),_.isUndefined(s.current))||(i.model=s.current,i.render(),this.setNavButtonsState(),n(".previous-theme").trigger("focus"))}),this.listenTo(i,"preview:close",function(){s.current=s.model}))},setNavButtonsState:function(){var e=n(".theme-install-overlay"),t=_.isUndefined(this.current)?this.model:this.current,i=e.find(".previous-theme"),e=e.find(".next-theme");0===this.model.collection.indexOf(t)&&(i.addClass("disabled").prop("disabled",!0),e.trigger("focus")),_.isUndefined(this.model.collection.at(this.model.collection.indexOf(t)+1))&&(e.addClass("disabled").prop("disabled",!0),i.trigger("focus"))},installTheme:function(e){var i=this;e.preventDefault(),wp.updates.maybeRequestFilesystemCredentials(e),n(document).on("wp-theme-install-success",function(e,t){i.model.get("id")===t.slug&&i.model.set({installed:!0}),t.blockTheme&&i.model.set({block_theme:!0})}),wp.updates.installTheme({slug:n(e.target).data("slug")})},updateTheme:function(e){var i=this;this.model.get("hasPackage")&&(e.preventDefault(),wp.updates.maybeRequestFilesystemCredentials(e),n(document).on("wp-theme-update-success",function(e,t){i.model.off("change",i.render,i),i.model.get("id")===t.slug&&i.model.set({hasUpdate:!1,version:t.newVersion}),i.model.on("change",i.render,i)}),wp.updates.updateTheme({slug:n(e.target).parents("div.theme").first().data("slug")}))}}),o.view.Details=wp.Backbone.View.extend({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"},html:o.template("theme-single"),render:function(){var e=this.model.toJSON();this.$el.html(this.html(e)),this.activeTheme(),this.navigation(),this.screenshotCheck(this.$el),this.containFocus(this.$el)},activeTheme:function(){this.$el.toggleClass("active",this.model.get("active"))},containFocus:function(s){_.delay(function(){n(".theme-overlay").trigger("focus")},100),s.on("keydown.wp-themes",function(e){var t=s.find(".theme-header button:not(.disabled)").first(),i=s.find(".theme-actions a:visible").last();9===e.which&&(t[0]===e.target&&e.shiftKey?(i.trigger("focus"),e.preventDefault()):i[0]!==e.target||e.shiftKey||(t.trigger("focus"),e.preventDefault()))})},collapse:function(e){var t,i=this;e=e||window.event,1!==o.data.themes.length&&(n(e.target).is(".theme-backdrop")||n(e.target).is(".close")||27===e.keyCode)&&(n("body").addClass("closing-overlay"),this.$el.fadeOut(130,function(){n("body").removeClass("closing-overlay"),i.closeOverlay(),t=document.body.scrollTop,o.router.navigate(o.router.baseUrl("")),document.body.scrollTop=t,o.focusedTheme&&o.focusedTheme.find(".more-details").trigger("focus")}))},navigation:function(){this.model.cid===this.model.collection.at(0).cid&&this.$el.find(".left").addClass("disabled").prop("disabled",!0),this.model.cid===this.model.collection.at(this.model.collection.length-1).cid&&this.$el.find(".right").addClass("disabled").prop("disabled",!0)},closeOverlay:function(){n("body").removeClass("modal-open"),this.remove(),this.unbind(),this.trigger("theme:collapse")},autoupdateState:function(){var s=this,r=function(e,t){var i;s.model.get("id")===t.asset&&((i=s.model.get("autoupdate")).enabled="enable"===t.state,s.model.set({autoupdate:i}),n(document).off("wp-auto-update-setting-changed",r))};n(document).on("wp-auto-update-setting-changed",r)},updateTheme:function(e){var i=this;e.preventDefault(),wp.updates.maybeRequestFilesystemCredentials(e),n(document).on("wp-theme-update-success",function(e,t){i.model.get("id")===t.slug&&i.model.set({hasUpdate:!1,version:t.newVersion}),i.render()}),wp.updates.updateTheme({slug:n(e.target).data("slug")})},deleteTheme:function(e){var i=this,s=i.model.collection,r=o;e.preventDefault(),window.confirm(wp.themes.data.settings.confirmDelete)&&(wp.updates.maybeRequestFilesystemCredentials(e),n(document).one("wp-theme-delete-success",function(e,t){i.$el.find(".close").trigger("click"),n('[data-slug="'+t.slug+'"]').css({backgroundColor:"#faafaa"}).fadeOut(350,function(){n(this).remove(),r.data.themes=_.without(r.data.themes,_.findWhere(r.data.themes,{id:t.slug})),n(".wp-filter-search").val(""),s.doSearch(""),s.remove(i.model),s.trigger("themes:update")})}),wp.updates.deleteTheme({slug:this.model.get("id")}))},nextTheme:function(){return this.trigger("theme:next",this.model.cid),!1},previousTheme:function(){return this.trigger("theme:previous",this.model.cid),!1},screenshotCheck:function(e){var t=e.find(".screenshot img"),i=new Image;i.src=t.attr("src"),i.width&&i.width<=300&&e.addClass("small-screenshot")}}),o.view.Preview=o.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"},html:o.template("theme-preview"),render:function(){var e=this,t=this.model.toJSON(),i=n(document.body);i.attr("aria-busy","true"),this.$el.removeClass("iframe-ready").html(this.html(t)),(t=this.$el.data("current-preview-device"))&&e.tooglePreviewDeviceButtons(t),o.router.navigate(o.router.baseUrl(o.router.themePath+this.model.get("id")),{replace:!1}),this.$el.fadeIn(200,function(){i.addClass("theme-installer-active full-overlay-active")}),this.$el.find("iframe").one("load",function(){e.iframeLoaded()})},iframeLoaded:function(){this.$el.addClass("iframe-ready"),n(document.body).attr("aria-busy","false")},close:function(){return this.$el.fadeOut(200,function(){n("body").removeClass("theme-installer-active full-overlay-active"),o.focusedTheme&&o.focusedTheme.find(".more-details").trigger("focus")}).removeClass("iframe-ready"),o.router.selectedTab?(o.router.navigate(o.router.baseUrl("?browse="+o.router.selectedTab)),o.router.selectedTab=!1):o.router.navigate(o.router.baseUrl("")),this.trigger("preview:close"),this.undelegateEvents(),this.unbind(),!1},collapse:function(e){e=n(e.currentTarget);return"true"===e.attr("aria-expanded")?e.attr({"aria-expanded":"false","aria-label":a.expandSidebar}):e.attr({"aria-expanded":"true","aria-label":a.collapseSidebar}),this.$el.toggleClass("collapsed").toggleClass("expanded"),!1},previewDevice:function(e){e=n(e.currentTarget).data("device");this.$el.removeClass("preview-desktop preview-tablet preview-mobile").addClass("preview-"+e).data("current-preview-device",e),this.tooglePreviewDeviceButtons(e)},tooglePreviewDeviceButtons:function(e){var t=n(".wp-full-overlay-footer .devices");t.find("button").removeClass("active").attr("aria-pressed",!1),t.find("button.preview-"+e).addClass("active").attr("aria-pressed",!0)},keyEvent:function(e){27===e.keyCode&&(this.undelegateEvents(),this.close()),39===e.keyCode&&_.once(this.nextTheme()),37===e.keyCode&&this.previousTheme()},installTheme:function(e){var t=this,i=n(e.target);e.preventDefault(),i.hasClass("disabled")||(wp.updates.maybeRequestFilesystemCredentials(e),n(document).on("wp-theme-install-success",function(){t.model.set({installed:!0})}),wp.updates.installTheme({slug:i.data("slug")}))}}),o.view.Themes=wp.Backbone.View.extend({className:"themes wp-clearfix",$overlay:n("div.theme-overlay"),index:0,count:n(".wrap .theme-count"),liveThemeCount:0,initialize:function(e){var t=this;this.parent=e.parent,this.setView("grid"),t.currentTheme(),this.listenTo(t.collection,"themes:update",function(){t.parent.page=0,t.currentTheme(),t.render(this)}),this.listenTo(t.collection,"query:success",function(e){_.isNumber(e)?(t.count.text(e),t.announceSearchResults(e)):(t.count.text(t.collection.length),t.announceSearchResults(t.collection.length))}),this.listenTo(t.collection,"query:empty",function(){n("body").addClass("no-results")}),this.listenTo(this.parent,"theme:scroll",function(){t.renderThemes(t.parent.page)}),this.listenTo(this.parent,"theme:close",function(){t.overlay&&t.overlay.closeOverlay()}),n("body").on("keyup",function(e){!t.overlay||n("#request-filesystem-credentials-dialog").is(":visible")||(39===e.keyCode&&t.overlay.nextTheme(),37===e.keyCode&&t.overlay.previousTheme(),27===e.keyCode&&t.overlay.collapse(e))})},render:function(){this.$el.empty(),1===o.data.themes.length&&(this.singleTheme=new o.view.Details({model:this.collection.models[0]}),this.singleTheme.render(),this.$el.addClass("single-theme"),this.$el.append(this.singleTheme.el)),0<this.options.collection.size()&&this.renderThemes(this.parent.page),this.liveThemeCount=this.collection.count||this.collection.length,this.count.text(this.liveThemeCount),o.isInstall||this.announceSearchResults(this.liveThemeCount)},renderThemes:function(e){var t=this;t.instance=t.collection.paginate(e),0===t.instance.size()?this.parent.trigger("theme:end"):(!o.isInstall&&1<=e&&n(".add-new-theme").remove(),t.instance.each(function(e){t.theme=new o.view.Theme({model:e,parent:t}),t.theme.render(),t.$el.append(t.theme.el),t.listenTo(t.theme,"theme:expand",t.expand,t)}),!o.isInstall&&o.data.settings.canInstall&&this.$el.append('<div class="theme add-new-theme"><a href="'+o.data.settings.installURI+'"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">'+a.addNew+"</h2></a></div>"),this.parent.page++)},currentTheme:function(){var e=this.collection.findWhere({active:!0});e&&(this.collection.remove(e),this.collection.add(e,{at:0}))},setView:function(e){return e},expand:function(e){var t,i=this;this.model=i.collection.get(e),o.router.navigate(o.router.baseUrl(o.router.themePath+this.model.id)),this.setView("detail"),n("body").addClass("modal-open"),this.overlay=new o.view.Details({model:i.model}),this.overlay.render(),this.model.get("hasUpdate")&&(e=n('[data-slug="'+this.model.id+'"]'),t=n(this.overlay.el),e.find(".updating-message").length?(t.find(".notice-warning h3").remove(),t.find(".notice-warning").removeClass("notice-large").addClass("updating-message").find("p").text(wp.updates.l10n.updating)):e.find(".notice-error").length&&t.find(".notice-warning").remove()),this.$overlay.html(this.overlay.el),this.listenTo(this.overlay,"theme:next",function(){i.next([i.model.cid])}).listenTo(this.overlay,"theme:previous",function(){i.previous([i.model.cid])})},next:function(e){e=this.collection.get(e[0]),e=this.collection.at(this.collection.indexOf(e)+1);void 0!==e&&(this.overlay.closeOverlay(),this.theme.trigger("theme:expand",e.cid))},previous:function(e){e=this.collection.get(e[0]),e=this.collection.at(this.collection.indexOf(e)-1);void 0!==e&&(this.overlay.closeOverlay(),this.theme.trigger("theme:expand",e.cid))},announceSearchResults:function(e){0===e?wp.a11y.speak(a.noThemesFound):wp.a11y.speak(a.themesFound.replace("%d",e))}}),o.view.Search=wp.Backbone.View.extend({tagName:"input",className:"wp-filter-search",id:"wp-filter-search-input",searching:!1,attributes:{placeholder:a.searchPlaceholder,type:"search","aria-describedby":"live-search-desc"},events:{input:"search",keyup:"search",blur:"pushState"},initialize:function(e){this.parent=e.parent,this.listenTo(this.parent,"theme:close",function(){this.searching=!1})},search:function(e){"keyup"===e.type&&27===e.which&&(e.target.value=""),this.doSearch(e)},doSearch:function(e){var t={};this.collection.doSearch(e.target.value.replace(/\+/g," ")),this.searching&&13!==e.which?t.replace=!0:this.searching=!0,e.target.value?o.router.navigate(o.router.baseUrl(o.router.searchPath+e.target.value),t):o.router.navigate(o.router.baseUrl(""))},pushState:function(e){var t=o.router.baseUrl("");e.target.value&&(t=o.router.baseUrl(o.router.searchPath+encodeURIComponent(e.target.value))),this.searching=!1,o.router.navigate(t)}}),o.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(e){return"themes.php"+e},themePath:"?theme=",searchPath:"?search=",search:function(e){n(".wp-filter-search").val(e.replace(/\+/g," "))},themes:function(){n(".wp-filter-search").val("")},navigate:e}),o.Run={init:function(){this.themes=new o.Collection(o.data.themes),this.view=new o.view.Appearance({collection:this.themes}),this.render(),this.view.SearchView.doSearch=_.debounce(this.view.SearchView.doSearch,500)},render:function(){this.view.render(),this.routes(),Backbone.History.started&&Backbone.history.stop(),Backbone.history.start({root:o.data.settings.adminUrl,pushState:!0,hashChange:!1})},routes:function(){var t=this;o.router=new o.Router,o.router.on("route:theme",function(e){t.view.view.expand(e)}),o.router.on("route:themes",function(){t.themes.doSearch(""),t.view.trigger("theme:close")}),o.router.on("route:search",function(){n(".wp-filter-search").trigger("keyup")}),this.extraRoutes()},extraRoutes:function(){return!1}},o.view.InstallerSearch=o.view.Search.extend({events:{input:"search",keyup:"search"},terms:"",search:function(e){("keyup"!==e.type||9!==e.which&&16!==e.which)&&(this.collection=this.options.parent.view.collection,"keyup"===e.type&&27===e.which&&(e.target.value=""),this.doSearch(e.target.value))},doSearch:function(e){var t={};this.terms!==e&&(this.terms=e,"author:"===(t.search=e).substring(0,7)&&(t.search="",t.author=e.slice(7)),"tag:"===e.substring(0,4)&&(t.search="",t.tag=[e.slice(4)]),n(".filter-links li > a.current").removeClass("current").removeAttr("aria-current"),n("body").removeClass("show-filters filters-applied show-favorites-form"),n(".drawer-toggle").attr("aria-expanded","false"),this.collection.query(t),o.router.navigate(o.router.baseUrl(o.router.searchPath+encodeURIComponent(e)),{replace:!0}))}}),o.view.Installer=o.view.Appearance.extend({el:"#wpbody-content .wrap",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"},render:function(){var e=this;this.search(),this.uploader(),this.collection=new o.Collection,this.listenTo(this,"theme:end",function(){e.collection.loadingThemes||(e.collection.loadingThemes=!0,e.collection.currentQuery.page++,_.extend(e.collection.currentQuery.request,{page:e.collection.currentQuery.page}),e.collection.query(e.collection.currentQuery.request))}),this.listenTo(this.collection,"query:success",function(){n("body").removeClass("loading-content"),n(".theme-browser").find("div.error").remove()}),this.listenTo(this.collection,"query:fail",function(){n("body").removeClass("loading-content"),n(".theme-browser").find("div.error").remove(),n(".theme-browser").find("div.themes").before('<div class="error"><p>'+a.error+'</p><p><button class="button try-again">'+a.tryAgain+"</button></p></div>"),n(".theme-browser .error .try-again").on("click",function(e){e.preventDefault(),n("input.wp-filter-search").trigger("input")})}),this.view&&this.view.remove(),this.view=new o.view.Themes({collection:this.collection,parent:this}),this.page=0,this.$el.find(".themes").remove(),this.view.render(),this.$el.find(".theme-browser").append(this.view.el).addClass("rendered")},browse:function(e){"block-themes"===e?this.collection.query({tag:"full-site-editing"}):this.collection.query({browse:e})},onSort:function(e){var t=n(e.target),i=t.data("sort");e.preventDefault(),n("body").removeClass("filters-applied show-filters"),n(".drawer-toggle").attr("aria-expanded","false"),t.hasClass(this.activeClass)||(this.sort(i),o.router.navigate(o.router.baseUrl(o.router.browsePath+i)))},sort:function(e){this.clearSearch(),o.router.selectedTab=e,n(".filter-links li > a, .theme-filter").removeClass(this.activeClass).removeAttr("aria-current"),n('[data-sort="'+e+'"]').addClass(this.activeClass).attr("aria-current","page"),"favorites"===e?n("body").addClass("show-favorites-form"):n("body").removeClass("show-favorites-form"),this.browse(e)},onFilter:function(e){var e=n(e.target),t=e.data("filter");e.hasClass(this.activeClass)||(n(".filter-links li > a, .theme-section").removeClass(this.activeClass).removeAttr("aria-current"),e.addClass(this.activeClass).attr("aria-current","page"),t&&(t=_.union([t,this.filtersChecked()]),this.collection.query({tag:[t]})))},addFilter:function(){this.filtersChecked()},applyFilters:function(e){var t,i=this.filtersChecked(),s={tag:i},r=n(".filtered-by .tags");e&&e.preventDefault(),i?(n("body").addClass("filters-applied"),n(".filter-links li > a.current").removeClass("current").removeAttr("aria-current"),r.empty(),_.each(i,function(e){t=n('label[for="filter-id-'+e+'"]').text(),r.append('<span class="tag">'+t+"</span>")}),this.collection.query(s)):wp.a11y.speak(a.selectFeatureFilter)},saveUsername:function(e){var t=n("#wporg-username-input").val(),i=n("#wporg-username-nonce").val(),s={browse:"favorites",user:t},r=this;if(e&&e.preventDefault(),"keyup"!==e.type||13===e.which)return wp.ajax.send("save-wporg-username",{data:{_wpnonce:i,username:t},success:function(){r.collection.query(s)}})},filtersChecked:function(){var e=n(".filter-group").find(":checkbox"),t=[];return _.each(e.filter(":checked"),function(e){t.push(n(e).prop("value"))}),0===t.length?(n(".filter-drawer .apply-filters").find("span").text(""),n(".filter-drawer .clear-filters").hide(),n("body").removeClass("filters-applied"),!1):(n(".filter-drawer .apply-filters").find("span").text(t.length),n(".filter-drawer .clear-filters").css("display","inline-block"),t)},activeClass:"current",uploader:function(){var e=n(".upload-view-toggle"),t=n(document.body);e.on("click",function(){t.toggleClass("show-upload-view"),e.attr("aria-expanded",t.hasClass("show-upload-view"))})},moreFilters:function(e){var t=n("body"),i=n(".drawer-toggle");if(e.preventDefault(),t.hasClass("filters-applied"))return this.backToFilters();this.clearSearch(),o.router.navigate(o.router.baseUrl("")),t.toggleClass("show-filters"),i.attr("aria-expanded",t.hasClass("show-filters"))},clearFilters:function(e){var t=n(".filter-group").find(":checkbox"),i=this;e.preventDefault(),_.each(t.filter(":checked"),function(e){return n(e).prop("checked",!1),i.filtersChecked()})},backToFilters:function(e){e&&e.preventDefault(),n("body").removeClass("filters-applied")},clearSearch:function(){n("#wp-filter-search-input").val("")}}),o.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(e){return"theme-install.php"+e},themePath:"?theme=",browsePath:"?browse=",searchPath:"?search=",search:function(e){n(".wp-filter-search").val(e.replace(/\+/g," "))},navigate:e}),o.RunInstaller={init:function(){this.view=new o.view.Installer({section:"popular",SearchView:o.view.InstallerSearch}),this.render(),this.view.SearchView.doSearch=_.debounce(this.view.SearchView.doSearch,500)},render:function(){this.view.render(),this.routes(),Backbone.History.started&&Backbone.history.stop(),Backbone.history.start({root:o.data.settings.adminUrl,pushState:!0,hashChange:!1})},routes:function(){var t=this,i={};o.router=new o.InstallerRouter,o.router.on("route:preview",function(e){o.preview&&(o.preview.undelegateEvents(),o.preview.unbind()),t.view.view.theme&&t.view.view.theme.preview?(t.view.view.theme.model=t.view.collection.findWhere({slug:e}),t.view.view.theme.preview()):(i.theme=e,t.view.collection.query(i),t.view.collection.trigger("update"),t.view.collection.once("query:success",function(){n('div[data-slug="'+e+'"]').trigger("click")}))}),o.router.on("route:sort",function(e){e||(e="popular",o.router.navigate(o.router.baseUrl("?browse=popular"),{replace:!0})),t.view.sort(e),o.preview&&o.preview.close()}),o.router.on("route:search",function(){n(".wp-filter-search").trigger("focus").trigger("keyup")}),this.extraRoutes()},extraRoutes:function(){return!1}},n(function(){(o.isInstall?o.RunInstaller:o.Run).init(),n(document.body).on("click",".load-customize",function(){var e=n(this),t=document.createElement("a");t.href=e.prop("href"),t.search=n.param(_.extend(wp.customize.utils.parseQueryString(t.search.substr(1)),{return:window.location.href})),e.prop("href",t.href)}),n(".broken-themes .delete-theme").on("click",function(){return confirm(_wpThemeSettings.settings.confirmDelete)})})}(jQuery),jQuery(function(r){window.tb_position=function(){var e=r("#TB_window"),t=r(window).width(),i=r(window).height(),t=1040<t?1040:t,s=0;r("#wpadminbar").length&&(s=parseInt(r("#wpadminbar").css("height"),10)),1<=e.length&&(e.width(t-50).height(i-45-s),r("#TB_iframeContent").width(t-50).height(i-75-s),e.css({"margin-left":"-"+parseInt((t-50)/2,10)+"px"}),void 0!==document.body.style.maxWidth)&&e.css({top:20+s+"px","margin-top":"0"})},r(window).on("resize",function(){tb_position()})});
\ No newline at end of file diff --git a/wp-admin/js/updates.js b/wp-admin/js/updates.js new file mode 100644 index 0000000..a994fda --- /dev/null +++ b/wp-admin/js/updates.js @@ -0,0 +1,3020 @@ +/** + * Functions for ajaxified updates, deletions and installs inside the WordPress admin. + * + * @version 4.2.0 + * @output wp-admin/js/updates.js + */ + +/* global pagenow, _wpThemeSettings */ + +/** + * @param {jQuery} $ jQuery object. + * @param {object} wp WP object. + * @param {object} settings WP Updates settings. + * @param {string} settings.ajax_nonce Ajax nonce. + * @param {object=} settings.plugins Base names of plugins in their different states. + * @param {Array} settings.plugins.all Base names of all plugins. + * @param {Array} settings.plugins.active Base names of active plugins. + * @param {Array} settings.plugins.inactive Base names of inactive plugins. + * @param {Array} settings.plugins.upgrade Base names of plugins with updates available. + * @param {Array} settings.plugins.recently_activated Base names of recently activated plugins. + * @param {Array} settings.plugins['auto-update-enabled'] Base names of plugins set to auto-update. + * @param {Array} settings.plugins['auto-update-disabled'] Base names of plugins set to not auto-update. + * @param {object=} settings.themes Slugs of themes in their different states. + * @param {Array} settings.themes.all Slugs of all themes. + * @param {Array} settings.themes.upgrade Slugs of themes with updates available. + * @param {Arrat} settings.themes.disabled Slugs of disabled themes. + * @param {Array} settings.themes['auto-update-enabled'] Slugs of themes set to auto-update. + * @param {Array} settings.themes['auto-update-disabled'] Slugs of themes set to not auto-update. + * @param {object=} settings.totals Combined information for available update counts. + * @param {number} settings.totals.count Holds the amount of available updates. + */ +(function( $, wp, settings ) { + var $document = $( document ), + __ = wp.i18n.__, + _x = wp.i18n._x, + _n = wp.i18n._n, + _nx = wp.i18n._nx, + sprintf = wp.i18n.sprintf; + + wp = wp || {}; + + /** + * The WP Updates object. + * + * @since 4.2.0 + * + * @namespace wp.updates + */ + wp.updates = {}; + + /** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 4.2.0 + * @deprecated 5.5.0 + * + * @type {object} + */ + wp.updates.l10n = { + searchResults: '', + searchResultsLabel: '', + noPlugins: '', + noItemsSelected: '', + updating: '', + pluginUpdated: '', + themeUpdated: '', + update: '', + updateNow: '', + pluginUpdateNowLabel: '', + updateFailedShort: '', + updateFailed: '', + pluginUpdatingLabel: '', + pluginUpdatedLabel: '', + pluginUpdateFailedLabel: '', + updatingMsg: '', + updatedMsg: '', + updateCancel: '', + beforeunload: '', + installNow: '', + pluginInstallNowLabel: '', + installing: '', + pluginInstalled: '', + themeInstalled: '', + installFailedShort: '', + installFailed: '', + pluginInstallingLabel: '', + themeInstallingLabel: '', + pluginInstalledLabel: '', + themeInstalledLabel: '', + pluginInstallFailedLabel: '', + themeInstallFailedLabel: '', + installingMsg: '', + installedMsg: '', + importerInstalledMsg: '', + aysDelete: '', + aysDeleteUninstall: '', + aysBulkDelete: '', + aysBulkDeleteThemes: '', + deleting: '', + deleteFailed: '', + pluginDeleted: '', + themeDeleted: '', + livePreview: '', + activatePlugin: '', + activateTheme: '', + activatePluginLabel: '', + activateThemeLabel: '', + activateImporter: '', + activateImporterLabel: '', + unknownError: '', + connectionError: '', + nonceError: '', + pluginsFound: '', + noPluginsFound: '', + autoUpdatesEnable: '', + autoUpdatesEnabling: '', + autoUpdatesEnabled: '', + autoUpdatesDisable: '', + autoUpdatesDisabling: '', + autoUpdatesDisabled: '', + autoUpdatesError: '' + }; + + wp.updates.l10n = window.wp.deprecateL10nObject( 'wp.updates.l10n', wp.updates.l10n, '5.5.0' ); + + /** + * User nonce for ajax calls. + * + * @since 4.2.0 + * + * @type {string} + */ + wp.updates.ajaxNonce = settings.ajax_nonce; + + /** + * Current search term. + * + * @since 4.6.0 + * + * @type {string} + */ + wp.updates.searchTerm = ''; + + /** + * Whether filesystem credentials need to be requested from the user. + * + * @since 4.2.0 + * + * @type {bool} + */ + wp.updates.shouldRequestFilesystemCredentials = false; + + /** + * Filesystem credentials to be packaged along with the request. + * + * @since 4.2.0 + * @since 4.6.0 Added `available` property to indicate whether credentials have been provided. + * + * @type {Object} + * @property {Object} filesystemCredentials.ftp Holds FTP credentials. + * @property {string} filesystemCredentials.ftp.host FTP host. Default empty string. + * @property {string} filesystemCredentials.ftp.username FTP user name. Default empty string. + * @property {string} filesystemCredentials.ftp.password FTP password. Default empty string. + * @property {string} filesystemCredentials.ftp.connectionType Type of FTP connection. 'ssh', 'ftp', or 'ftps'. + * Default empty string. + * @property {Object} filesystemCredentials.ssh Holds SSH credentials. + * @property {string} filesystemCredentials.ssh.publicKey The public key. Default empty string. + * @property {string} filesystemCredentials.ssh.privateKey The private key. Default empty string. + * @property {string} filesystemCredentials.fsNonce Filesystem credentials form nonce. + * @property {bool} filesystemCredentials.available Whether filesystem credentials have been provided. + * Default 'false'. + */ + wp.updates.filesystemCredentials = { + ftp: { + host: '', + username: '', + password: '', + connectionType: '' + }, + ssh: { + publicKey: '', + privateKey: '' + }, + fsNonce: '', + available: false + }; + + /** + * Whether we're waiting for an Ajax request to complete. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `ajaxLocked`. + * + * @type {bool} + */ + wp.updates.ajaxLocked = false; + + /** + * Admin notice template. + * + * @since 4.6.0 + * + * @type {function} + */ + wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); + + /** + * Update queue. + * + * If the user tries to update a plugin while an update is + * already happening, it can be placed in this queue to perform later. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `queue`. + * + * @type {Array.object} + */ + wp.updates.queue = []; + + /** + * Holds a jQuery reference to return focus to when exiting the request credentials modal. + * + * @since 4.2.0 + * + * @type {jQuery} + */ + wp.updates.$elToReturnFocusToFromCredentialsModal = undefined; + + /** + * Adds or updates an admin notice. + * + * @since 4.6.0 + * + * @param {Object} data + * @param {*=} data.selector Optional. Selector of an element to be replaced with the admin notice. + * @param {string=} data.id Optional. Unique id that will be used as the notice's id attribute. + * @param {string=} data.className Optional. Class names that will be used in the admin notice. + * @param {string=} data.message Optional. The message displayed in the notice. + * @param {number=} data.successes Optional. The amount of successful operations. + * @param {number=} data.errors Optional. The amount of failed operations. + * @param {Array=} data.errorMessages Optional. Error messages of failed operations. + * + */ + wp.updates.addAdminNotice = function( data ) { + var $notice = $( data.selector ), + $headerEnd = $( '.wp-header-end' ), + $adminNotice; + + delete data.selector; + $adminNotice = wp.updates.adminNotice( data ); + + // Check if this admin notice already exists. + if ( ! $notice.length ) { + $notice = $( '#' + data.id ); + } + + if ( $notice.length ) { + $notice.replaceWith( $adminNotice ); + } else if ( $headerEnd.length ) { + $headerEnd.after( $adminNotice ); + } else { + if ( 'customize' === pagenow ) { + $( '.customize-themes-notifications' ).append( $adminNotice ); + } else { + $( '.wrap' ).find( '> h1' ).after( $adminNotice ); + } + } + + $document.trigger( 'wp-updates-notice-added' ); + }; + + /** + * Handles Ajax requests to WordPress. + * + * @since 4.6.0 + * + * @param {string} action The type of Ajax request ('update-plugin', 'install-theme', etc). + * @param {Object} data Data that needs to be passed to the ajax callback. + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.ajax = function( action, data ) { + var options = {}; + + if ( wp.updates.ajaxLocked ) { + wp.updates.queue.push( { + action: action, + data: data + } ); + + // Return a Deferred object so callbacks can always be registered. + return $.Deferred(); + } + + wp.updates.ajaxLocked = true; + + if ( data.success ) { + options.success = data.success; + delete data.success; + } + + if ( data.error ) { + options.error = data.error; + delete data.error; + } + + options.data = _.extend( data, { + action: action, + _ajax_nonce: wp.updates.ajaxNonce, + _fs_nonce: wp.updates.filesystemCredentials.fsNonce, + username: wp.updates.filesystemCredentials.ftp.username, + password: wp.updates.filesystemCredentials.ftp.password, + hostname: wp.updates.filesystemCredentials.ftp.hostname, + connection_type: wp.updates.filesystemCredentials.ftp.connectionType, + public_key: wp.updates.filesystemCredentials.ssh.publicKey, + private_key: wp.updates.filesystemCredentials.ssh.privateKey + } ); + + return wp.ajax.send( options ).always( wp.updates.ajaxAlways ); + }; + + /** + * Actions performed after every Ajax request. + * + * @since 4.6.0 + * + * @param {Object} response + * @param {Array=} response.debug Optional. Debug information. + * @param {string=} response.errorCode Optional. Error code for an error that occurred. + */ + wp.updates.ajaxAlways = function( response ) { + if ( ! response.errorCode || 'unable_to_connect_to_filesystem' !== response.errorCode ) { + wp.updates.ajaxLocked = false; + wp.updates.queueChecker(); + } + + if ( 'undefined' !== typeof response.debug && window.console && window.console.log ) { + _.map( response.debug, function( message ) { + // Remove all HTML tags and write a message to the console. + window.console.log( wp.sanitize.stripTagsAndEncodeText( message ) ); + } ); + } + }; + + /** + * Refreshes update counts everywhere on the screen. + * + * @since 4.7.0 + */ + wp.updates.refreshCount = function() { + var $adminBarUpdates = $( '#wp-admin-bar-updates' ), + $dashboardNavMenuUpdateCount = $( 'a[href="update-core.php"] .update-plugins' ), + $pluginsNavMenuUpdateCount = $( 'a[href="plugins.php"] .update-plugins' ), + $appearanceNavMenuUpdateCount = $( 'a[href="themes.php"] .update-plugins' ), + itemCount; + + $adminBarUpdates.find( '.ab-label' ).text( settings.totals.counts.total ); + $adminBarUpdates.find( '.updates-available-text' ).text( + sprintf( + /* translators: %s: Total number of updates available. */ + _n( '%s update available', '%s updates available', settings.totals.counts.total ), + settings.totals.counts.total + ) + ); + + // Remove the update count from the toolbar if it's zero. + if ( 0 === settings.totals.counts.total ) { + $adminBarUpdates.find( '.ab-label' ).parents( 'li' ).remove(); + } + + // Update the "Updates" menu item. + $dashboardNavMenuUpdateCount.each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.total ); + } ); + if ( settings.totals.counts.total > 0 ) { + $dashboardNavMenuUpdateCount.find( '.update-count' ).text( settings.totals.counts.total ); + } else { + $dashboardNavMenuUpdateCount.remove(); + } + + // Update the "Plugins" menu item. + $pluginsNavMenuUpdateCount.each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.plugins ); + } ); + if ( settings.totals.counts.total > 0 ) { + $pluginsNavMenuUpdateCount.find( '.plugin-count' ).text( settings.totals.counts.plugins ); + } else { + $pluginsNavMenuUpdateCount.remove(); + } + + // Update the "Appearance" menu item. + $appearanceNavMenuUpdateCount.each( function( index, element ) { + element.className = element.className.replace( /count-\d+/, 'count-' + settings.totals.counts.themes ); + } ); + if ( settings.totals.counts.total > 0 ) { + $appearanceNavMenuUpdateCount.find( '.theme-count' ).text( settings.totals.counts.themes ); + } else { + $appearanceNavMenuUpdateCount.remove(); + } + + // Update list table filter navigation. + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + itemCount = settings.totals.counts.plugins; + } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { + itemCount = settings.totals.counts.themes; + } + + if ( itemCount > 0 ) { + $( '.subsubsub .upgrade .count' ).text( '(' + itemCount + ')' ); + } else { + $( '.subsubsub .upgrade' ).remove(); + $( '.subsubsub li:last' ).html( function() { return $( this ).children(); } ); + } + }; + + /** + * Decrements the update counts throughout the various menus. + * + * This includes the toolbar, the "Updates" menu item and the menu items + * for plugins and themes. + * + * @since 3.9.0 + * + * @param {string} type The type of item that was updated or deleted. + * Can be 'plugin', 'theme'. + */ + wp.updates.decrementCount = function( type ) { + settings.totals.counts.total = Math.max( --settings.totals.counts.total, 0 ); + + if ( 'plugin' === type ) { + settings.totals.counts.plugins = Math.max( --settings.totals.counts.plugins, 0 ); + } else if ( 'theme' === type ) { + settings.totals.counts.themes = Math.max( --settings.totals.counts.themes, 0 ); + } + + wp.updates.refreshCount( type ); + }; + + /** + * Sends an Ajax request to the server to update a plugin. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePlugin`. + * + * @param {Object} args Arguments. + * @param {string} args.plugin Plugin basename. + * @param {string} args.slug Plugin slug. + * @param {updatePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.updatePluginSuccess + * @param {updatePluginError=} args.error Optional. Error callback. Default: wp.updates.updatePluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.updatePlugin = function( args ) { + var $updateRow, $card, $message, message, + $adminBarUpdates = $( '#wp-admin-bar-updates' ); + + args = _.extend( { + success: wp.updates.updatePluginSuccess, + error: wp.updates.updatePluginError + }, args ); + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $updateRow = $( 'tr[data-plugin="' + args.plugin + '"]' ); + $message = $updateRow.find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); + message = sprintf( + /* translators: %s: Plugin name and version. */ + _x( 'Updating %s...', 'plugin' ), + $updateRow.find( '.plugin-title strong' ).text() + ); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $card = $( '.plugin-card-' + args.slug ); + $message = $card.find( '.update-now' ).addClass( 'updating-message' ); + message = sprintf( + /* translators: %s: Plugin name and version. */ + _x( 'Updating %s...', 'plugin' ), + $message.data( 'name' ) + ); + + // Remove previous error messages, if any. + $card.removeClass( 'plugin-card-update-failed' ).find( '.notice.notice-error' ).remove(); + } + + $adminBarUpdates.addClass( 'spin' ); + + if ( $message.html() !== __( 'Updating...' ) ) { + $message.data( 'originaltext', $message.html() ); + } + + $message + .attr( 'aria-label', message ) + .text( __( 'Updating...' ) ); + + $document.trigger( 'wp-plugin-updating', args ); + + return wp.updates.ajax( 'update-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin update. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePluginSuccess`. + * @since 5.5.0 Auto-update "time to next update" text cleared. + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be updated. + * @param {string} response.plugin Basename of the plugin to be updated. + * @param {string} response.pluginName Name of the plugin to be updated. + * @param {string} response.oldVersion Old version of the plugin. + * @param {string} response.newVersion New version of the plugin. + */ + wp.updates.updatePluginSuccess = function( response ) { + var $pluginRow, $updateMessage, newText, + $adminBarUpdates = $( '#wp-admin-bar-updates' ); + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ) + .removeClass( 'update is-enqueued' ) + .addClass( 'updated' ); + $updateMessage = $pluginRow.find( '.update-message' ) + .removeClass( 'updating-message notice-warning' ) + .addClass( 'updated-message notice-success' ).find( 'p' ); + + // Update the version number in the row. + newText = $pluginRow.find( '.plugin-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); + $pluginRow.find( '.plugin-version-author-uri' ).html( newText ); + + // Clear the "time to next auto-update" text. + $pluginRow.find( '.auto-update-time' ).empty(); + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $updateMessage = $( '.plugin-card-' + response.slug ).find( '.update-now' ) + .removeClass( 'updating-message' ) + .addClass( 'button-disabled updated-message' ); + } + + $adminBarUpdates.removeClass( 'spin' ); + + $updateMessage + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( '%s updated!', 'plugin' ), + response.pluginName + ) + ) + .text( _x( 'Updated!', 'plugin' ) ); + + wp.a11y.speak( __( 'Update completed successfully.' ) ); + + wp.updates.decrementCount( 'plugin' ); + + $document.trigger( 'wp-plugin-update-success', response ); + }; + + /** + * Updates the UI appropriately after a failed plugin update. + * + * @since 4.2.0 + * @since 4.6.0 More accurately named `updatePluginError`. + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be updated. + * @param {string} response.plugin Basename of the plugin to be updated. + * @param {string=} response.pluginName Optional. Name of the plugin to be updated. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.updatePluginError = function( response ) { + var $pluginRow, $card, $message, errorMessage, + $adminBarUpdates = $( '#wp-admin-bar-updates' ); + + if ( ! wp.updates.isValidResponse( response, 'update' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'update-plugin' ) ) { + return; + } + + errorMessage = sprintf( + /* translators: %s: Error string for a failed update. */ + __( 'Update failed: %s' ), + response.errorMessage + ); + + if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + $pluginRow = $( 'tr[data-plugin="' + response.plugin + '"]' ).removeClass( 'is-enqueued' ); + + if ( response.plugin ) { + $message = $( 'tr[data-plugin="' + response.plugin + '"]' ).find( '.update-message' ); + } else { + $message = $( 'tr[data-slug="' + response.slug + '"]' ).find( '.update-message' ); + } + $message.removeClass( 'updating-message notice-warning' ).addClass( 'notice-error' ).find( 'p' ).html( errorMessage ); + + if ( response.pluginName ) { + $message.find( 'p' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( '%s update failed.', 'plugin' ), + response.pluginName + ) + ); + } else { + $message.find( 'p' ).removeAttr( 'aria-label' ); + } + } else if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + $card = $( '.plugin-card-' + response.slug ) + .addClass( 'plugin-card-update-failed' ) + .append( wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt is-dismissible', + message: errorMessage + } ) ); + + $card.find( '.update-now' ) + .text( __( 'Update failed.' ) ) + .removeClass( 'updating-message' ); + + if ( response.pluginName ) { + $card.find( '.update-now' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( '%s update failed.', 'plugin' ), + response.pluginName + ) + ); + } else { + $card.find( '.update-now' ).removeAttr( 'aria-label' ); + } + + $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { + + // Use same delay as the total duration of the notice fadeTo + slideUp animation. + setTimeout( function() { + $card + .removeClass( 'plugin-card-update-failed' ) + .find( '.column-name a' ).trigger( 'focus' ); + + $card.find( '.update-now' ) + .attr( 'aria-label', false ) + .text( __( 'Update Now' ) ); + }, 200 ); + } ); + } + + $adminBarUpdates.removeClass( 'spin' ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-plugin-update-error', response ); + }; + + /** + * Sends an Ajax request to the server to install a plugin. + * + * @since 4.6.0 + * + * @param {Object} args Arguments. + * @param {string} args.slug Plugin identifier in the WordPress.org Plugin repository. + * @param {installPluginSuccess=} args.success Optional. Success callback. Default: wp.updates.installPluginSuccess + * @param {installPluginError=} args.error Optional. Error callback. Default: wp.updates.installPluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.installPlugin = function( args ) { + var $card = $( '.plugin-card-' + args.slug ), + $message = $card.find( '.install-now' ); + + args = _.extend( { + success: wp.updates.installPluginSuccess, + error: wp.updates.installPluginError + }, args ); + + if ( 'import' === pagenow ) { + $message = $( '[data-slug="' + args.slug + '"]' ); + } + + if ( $message.html() !== __( 'Installing...' ) ) { + $message.data( 'originaltext', $message.html() ); + } + + $message + .addClass( 'updating-message' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( 'Installing %s...', 'plugin' ), + $message.data( 'name' ) + ) + ) + .text( __( 'Installing...' ) ); + + wp.a11y.speak( __( 'Installing... please wait.' ) ); + + // Remove previous error messages, if any. + $card.removeClass( 'plugin-card-install-failed' ).find( '.notice.notice-error' ).remove(); + + $document.trigger( 'wp-plugin-installing', args ); + + return wp.updates.ajax( 'install-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin install. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the installed plugin. + * @param {string} response.pluginName Name of the installed plugin. + * @param {string} response.activateUrl URL to activate the just installed plugin. + */ + wp.updates.installPluginSuccess = function( response ) { + var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' ); + + $message + .removeClass( 'updating-message' ) + .addClass( 'updated-message installed button-disabled' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( '%s installed!', 'plugin' ), + response.pluginName + ) + ) + .text( _x( 'Installed!', 'plugin' ) ); + + wp.a11y.speak( __( 'Installation completed successfully.' ) ); + + $document.trigger( 'wp-plugin-install-success', response ); + + if ( response.activateUrl ) { + setTimeout( function() { + + // Transform the 'Install' button into an 'Activate' button. + $message.removeClass( 'install-now installed button-disabled updated-message' ) + .addClass( 'activate-now button-primary' ) + .attr( 'href', response.activateUrl ); + + if ( 'plugins-network' === pagenow ) { + $message + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name. */ + _x( 'Network Activate %s', 'plugin' ), + response.pluginName + ) + ) + .text( __( 'Network Activate' ) ); + } else { + $message + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name. */ + _x( 'Activate %s', 'plugin' ), + response.pluginName + ) + ) + .text( __( 'Activate' ) ); + } + }, 1000 ); + } + }; + + /** + * Updates the UI appropriately after a failed plugin install. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be installed. + * @param {string=} response.pluginName Optional. Name of the plugin to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.installPluginError = function( response ) { + var $card = $( '.plugin-card-' + response.slug ), + $button = $card.find( '.install-now' ), + errorMessage; + + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { + return; + } + + errorMessage = sprintf( + /* translators: %s: Error string for a failed installation. */ + __( 'Installation failed: %s' ), + response.errorMessage + ); + + $card + .addClass( 'plugin-card-update-failed' ) + .append( '<div class="notice notice-error notice-alt is-dismissible"><p>' + errorMessage + '</p></div>' ); + + $card.on( 'click', '.notice.is-dismissible .notice-dismiss', function() { + + // Use same delay as the total duration of the notice fadeTo + slideUp animation. + setTimeout( function() { + $card + .removeClass( 'plugin-card-update-failed' ) + .find( '.column-name a' ).trigger( 'focus' ); + }, 200 ); + } ); + + $button + .removeClass( 'updating-message' ).addClass( 'button-disabled' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( '%s installation failed', 'plugin' ), + $button.data( 'name' ) + ) + ) + .text( __( 'Installation failed.' ) ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-plugin-install-error', response ); + }; + + /** + * Updates the UI appropriately after a successful importer install. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the installed plugin. + * @param {string} response.pluginName Name of the installed plugin. + * @param {string} response.activateUrl URL to activate the just installed plugin. + */ + wp.updates.installImporterSuccess = function( response ) { + wp.updates.addAdminNotice( { + id: 'install-success', + className: 'notice-success is-dismissible', + message: sprintf( + /* translators: %s: Activation URL. */ + __( 'Importer installed successfully. <a href="%s">Run importer</a>' ), + response.activateUrl + '&from=import' + ) + } ); + + $( '[data-slug="' + response.slug + '"]' ) + .removeClass( 'install-now updating-message' ) + .addClass( 'activate-now' ) + .attr({ + 'href': response.activateUrl + '&from=import', + 'aria-label':sprintf( + /* translators: %s: Importer name. */ + __( 'Run %s' ), + response.pluginName + ) + }) + .text( __( 'Run Importer' ) ); + + wp.a11y.speak( __( 'Installation completed successfully.' ) ); + + $document.trigger( 'wp-importer-install-success', response ); + }; + + /** + * Updates the UI appropriately after a failed importer install. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be installed. + * @param {string=} response.pluginName Optional. Name of the plugin to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.installImporterError = function( response ) { + var errorMessage = sprintf( + /* translators: %s: Error string for a failed installation. */ + __( 'Installation failed: %s' ), + response.errorMessage + ), + $installLink = $( '[data-slug="' + response.slug + '"]' ), + pluginName = $installLink.data( 'name' ); + + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'install-plugin' ) ) { + return; + } + + wp.updates.addAdminNotice( { + id: response.errorCode, + className: 'notice-error is-dismissible', + message: errorMessage + } ); + + $installLink + .removeClass( 'updating-message' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name. */ + _x( 'Install %s now', 'plugin' ), + pluginName + ) + ) + .text( __( 'Install Now' ) ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-importer-install-error', response ); + }; + + /** + * Sends an Ajax request to the server to delete a plugin. + * + * @since 4.6.0 + * + * @param {Object} args Arguments. + * @param {string} args.plugin Basename of the plugin to be deleted. + * @param {string} args.slug Slug of the plugin to be deleted. + * @param {deletePluginSuccess=} args.success Optional. Success callback. Default: wp.updates.deletePluginSuccess + * @param {deletePluginError=} args.error Optional. Error callback. Default: wp.updates.deletePluginError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.deletePlugin = function( args ) { + var $link = $( '[data-plugin="' + args.plugin + '"]' ).find( '.row-actions a.delete' ); + + args = _.extend( { + success: wp.updates.deletePluginSuccess, + error: wp.updates.deletePluginError + }, args ); + + if ( $link.html() !== __( 'Deleting...' ) ) { + $link + .data( 'originaltext', $link.html() ) + .text( __( 'Deleting...' ) ); + } + + wp.a11y.speak( __( 'Deleting...' ) ); + + $document.trigger( 'wp-plugin-deleting', args ); + + return wp.updates.ajax( 'delete-plugin', args ); + }; + + /** + * Updates the UI appropriately after a successful plugin deletion. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the plugin that was deleted. + * @param {string} response.plugin Base name of the plugin that was deleted. + * @param {string} response.pluginName Name of the plugin that was deleted. + */ + wp.updates.deletePluginSuccess = function( response ) { + + // Removes the plugin and updates rows. + $( '[data-plugin="' + response.plugin + '"]' ).css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { + var $form = $( '#bulk-action-form' ), + $views = $( '.subsubsub' ), + $pluginRow = $( this ), + $currentView = $views.find( '[aria-current="page"]' ), + $itemsCount = $( '.displaying-num' ), + columnCount = $form.find( 'thead th:not(.hidden), thead td' ).length, + pluginDeletedRow = wp.template( 'item-deleted-row' ), + /** + * Plugins Base names of plugins in their different states. + * + * @type {Object} + */ + plugins = settings.plugins, + remainingCount; + + // Add a success message after deleting a plugin. + if ( ! $pluginRow.hasClass( 'plugin-update-tr' ) ) { + $pluginRow.after( + pluginDeletedRow( { + slug: response.slug, + plugin: response.plugin, + colspan: columnCount, + name: response.pluginName + } ) + ); + } + + $pluginRow.remove(); + + // Remove plugin from update count. + if ( -1 !== _.indexOf( plugins.upgrade, response.plugin ) ) { + plugins.upgrade = _.without( plugins.upgrade, response.plugin ); + wp.updates.decrementCount( 'plugin' ); + } + + // Remove from views. + if ( -1 !== _.indexOf( plugins.inactive, response.plugin ) ) { + plugins.inactive = _.without( plugins.inactive, response.plugin ); + if ( plugins.inactive.length ) { + $views.find( '.inactive .count' ).text( '(' + plugins.inactive.length + ')' ); + } else { + $views.find( '.inactive' ).remove(); + } + } + + if ( -1 !== _.indexOf( plugins.active, response.plugin ) ) { + plugins.active = _.without( plugins.active, response.plugin ); + if ( plugins.active.length ) { + $views.find( '.active .count' ).text( '(' + plugins.active.length + ')' ); + } else { + $views.find( '.active' ).remove(); + } + } + + if ( -1 !== _.indexOf( plugins.recently_activated, response.plugin ) ) { + plugins.recently_activated = _.without( plugins.recently_activated, response.plugin ); + if ( plugins.recently_activated.length ) { + $views.find( '.recently_activated .count' ).text( '(' + plugins.recently_activated.length + ')' ); + } else { + $views.find( '.recently_activated' ).remove(); + } + } + + if ( -1 !== _.indexOf( plugins['auto-update-enabled'], response.plugin ) ) { + plugins['auto-update-enabled'] = _.without( plugins['auto-update-enabled'], response.plugin ); + if ( plugins['auto-update-enabled'].length ) { + $views.find( '.auto-update-enabled .count' ).text( '(' + plugins['auto-update-enabled'].length + ')' ); + } else { + $views.find( '.auto-update-enabled' ).remove(); + } + } + + if ( -1 !== _.indexOf( plugins['auto-update-disabled'], response.plugin ) ) { + plugins['auto-update-disabled'] = _.without( plugins['auto-update-disabled'], response.plugin ); + if ( plugins['auto-update-disabled'].length ) { + $views.find( '.auto-update-disabled .count' ).text( '(' + plugins['auto-update-disabled'].length + ')' ); + } else { + $views.find( '.auto-update-disabled' ).remove(); + } + } + + plugins.all = _.without( plugins.all, response.plugin ); + + if ( plugins.all.length ) { + $views.find( '.all .count' ).text( '(' + plugins.all.length + ')' ); + } else { + $form.find( '.tablenav' ).css( { visibility: 'hidden' } ); + $views.find( '.all' ).remove(); + + if ( ! $form.find( 'tr.no-items' ).length ) { + $form.find( '#the-list' ).append( '<tr class="no-items"><td class="colspanchange" colspan="' + columnCount + '">' + __( 'No plugins are currently available.' ) + '</td></tr>' ); + } + } + + if ( $itemsCount.length && $currentView.length ) { + remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length; + $itemsCount.text( + sprintf( + /* translators: %s: The remaining number of plugins. */ + _nx( '%s item', '%s items', 'plugin/plugins', remainingCount ), + remainingCount + ) + ); + } + } ); + + wp.a11y.speak( _x( 'Deleted!', 'plugin' ) ); + + $document.trigger( 'wp-plugin-delete-success', response ); + }; + + /** + * Updates the UI appropriately after a failed plugin deletion. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the plugin to be deleted. + * @param {string} response.plugin Base name of the plugin to be deleted + * @param {string=} response.pluginName Optional. Name of the plugin to be deleted. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.deletePluginError = function( response ) { + var $plugin, $pluginUpdateRow, + pluginUpdateRow = wp.template( 'item-update-row' ), + noticeContent = wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt', + message: response.errorMessage + } ); + + if ( response.plugin ) { + $plugin = $( 'tr.inactive[data-plugin="' + response.plugin + '"]' ); + $pluginUpdateRow = $plugin.siblings( '[data-plugin="' + response.plugin + '"]' ); + } else { + $plugin = $( 'tr.inactive[data-slug="' + response.slug + '"]' ); + $pluginUpdateRow = $plugin.siblings( '[data-slug="' + response.slug + '"]' ); + } + + if ( ! wp.updates.isValidResponse( response, 'delete' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'delete-plugin' ) ) { + return; + } + + // Add a plugin update row if it doesn't exist yet. + if ( ! $pluginUpdateRow.length ) { + $plugin.addClass( 'update' ).after( + pluginUpdateRow( { + slug: response.slug, + plugin: response.plugin || response.slug, + colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, + content: noticeContent + } ) + ); + } else { + + // Remove previous error messages, if any. + $pluginUpdateRow.find( '.notice-error' ).remove(); + + $pluginUpdateRow.find( '.plugin-update' ).append( noticeContent ); + } + + $document.trigger( 'wp-plugin-delete-error', response ); + }; + + /** + * Sends an Ajax request to the server to update a theme. + * + * @since 4.6.0 + * + * @param {Object} args Arguments. + * @param {string} args.slug Theme stylesheet. + * @param {updateThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.updateThemeSuccess + * @param {updateThemeError=} args.error Optional. Error callback. Default: wp.updates.updateThemeError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.updateTheme = function( args ) { + var $notice; + + args = _.extend( { + success: wp.updates.updateThemeSuccess, + error: wp.updates.updateThemeError + }, args ); + + if ( 'themes-network' === pagenow ) { + $notice = $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ).removeClass( 'notice-error' ).addClass( 'updating-message notice-warning' ).find( 'p' ); + + } else if ( 'customize' === pagenow ) { + + // Update the theme details UI. + $notice = $( '[data-slug="' + args.slug + '"].notice' ).removeClass( 'notice-large' ); + + $notice.find( 'h3' ).remove(); + + // Add the top-level UI, and update both. + $notice = $notice.add( $( '#customize-control-installed_theme_' + args.slug ).find( '.update-message' ) ); + $notice = $notice.addClass( 'updating-message' ).find( 'p' ); + + } else { + $notice = $( '#update-theme' ).closest( '.notice' ).removeClass( 'notice-large' ); + + $notice.find( 'h3' ).remove(); + + $notice = $notice.add( $( '[data-slug="' + args.slug + '"]' ).find( '.update-message' ) ); + $notice = $notice.addClass( 'updating-message' ).find( 'p' ); + } + + if ( $notice.html() !== __( 'Updating...' ) ) { + $notice.data( 'originaltext', $notice.html() ); + } + + wp.a11y.speak( __( 'Updating... please wait.' ) ); + $notice.text( __( 'Updating...' ) ); + + $document.trigger( 'wp-theme-updating', args ); + + return wp.updates.ajax( 'update-theme', args ); + }; + + /** + * Updates the UI appropriately after a successful theme update. + * + * @since 4.6.0 + * @since 5.5.0 Auto-update "time to next update" text cleared. + * + * @param {Object} response + * @param {string} response.slug Slug of the theme to be updated. + * @param {Object} response.theme Updated theme. + * @param {string} response.oldVersion Old version of the theme. + * @param {string} response.newVersion New version of the theme. + */ + wp.updates.updateThemeSuccess = function( response ) { + var isModalOpen = $( 'body.modal-open' ).length, + $theme = $( '[data-slug="' + response.slug + '"]' ), + updatedMessage = { + className: 'updated-message notice-success notice-alt', + message: _x( 'Updated!', 'theme' ) + }, + $notice, newText; + + if ( 'customize' === pagenow ) { + $theme = $( '.updating-message' ).siblings( '.theme-name' ); + + if ( $theme.length ) { + + // Update the version number in the row. + newText = $theme.html().replace( response.oldVersion, response.newVersion ); + $theme.html( newText ); + } + + $notice = $( '.theme-info .notice' ).add( wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ).find( '.update-message' ) ); + } else if ( 'themes-network' === pagenow ) { + $notice = $theme.find( '.update-message' ); + + // Update the version number in the row. + newText = $theme.find( '.theme-version-author-uri' ).html().replace( response.oldVersion, response.newVersion ); + $theme.find( '.theme-version-author-uri' ).html( newText ); + + // Clear the "time to next auto-update" text. + $theme.find( '.auto-update-time' ).empty(); + } else { + $notice = $( '.theme-info .notice' ).add( $theme.find( '.update-message' ) ); + + // Focus on Customize button after updating. + if ( isModalOpen ) { + $( '.load-customize:visible' ).trigger( 'focus' ); + $( '.theme-info .theme-autoupdate' ).find( '.auto-update-time' ).empty(); + } else { + $theme.find( '.load-customize' ).trigger( 'focus' ); + } + } + + wp.updates.addAdminNotice( _.extend( { selector: $notice }, updatedMessage ) ); + wp.a11y.speak( __( 'Update completed successfully.' ) ); + + wp.updates.decrementCount( 'theme' ); + + $document.trigger( 'wp-theme-update-success', response ); + + // Show updated message after modal re-rendered. + if ( isModalOpen && 'customize' !== pagenow ) { + $( '.theme-info .theme-author' ).after( wp.updates.adminNotice( updatedMessage ) ); + } + }; + + /** + * Updates the UI appropriately after a failed theme update. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the theme to be updated. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.updateThemeError = function( response ) { + var $theme = $( '[data-slug="' + response.slug + '"]' ), + errorMessage = sprintf( + /* translators: %s: Error string for a failed update. */ + __( 'Update failed: %s' ), + response.errorMessage + ), + $notice; + + if ( ! wp.updates.isValidResponse( response, 'update' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'update-theme' ) ) { + return; + } + + if ( 'customize' === pagenow ) { + $theme = wp.customize.control( 'installed_theme_' + response.slug ).container.find( '.theme' ); + } + + if ( 'themes-network' === pagenow ) { + $notice = $theme.find( '.update-message ' ); + } else { + $notice = $( '.theme-info .notice' ).add( $theme.find( '.notice' ) ); + + $( 'body.modal-open' ).length ? $( '.load-customize:visible' ).trigger( 'focus' ) : $theme.find( '.load-customize' ).trigger( 'focus'); + } + + wp.updates.addAdminNotice( { + selector: $notice, + className: 'update-message notice-error notice-alt is-dismissible', + message: errorMessage + } ); + + wp.a11y.speak( errorMessage ); + + $document.trigger( 'wp-theme-update-error', response ); + }; + + /** + * Sends an Ajax request to the server to install a theme. + * + * @since 4.6.0 + * + * @param {Object} args + * @param {string} args.slug Theme stylesheet. + * @param {installThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.installThemeSuccess + * @param {installThemeError=} args.error Optional. Error callback. Default: wp.updates.installThemeError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.installTheme = function( args ) { + var $message = $( '.theme-install[data-slug="' + args.slug + '"]' ); + + args = _.extend( { + success: wp.updates.installThemeSuccess, + error: wp.updates.installThemeError + }, args ); + + $message.addClass( 'updating-message' ); + $message.parents( '.theme' ).addClass( 'focus' ); + if ( $message.html() !== __( 'Installing...' ) ) { + $message.data( 'originaltext', $message.html() ); + } + + $message + .attr( + 'aria-label', + sprintf( + /* translators: %s: Theme name and version. */ + _x( 'Installing %s...', 'theme' ), + $message.data( 'name' ) + ) + ) + .text( __( 'Installing...' ) ); + + wp.a11y.speak( __( 'Installing... please wait.' ) ); + + // Remove previous error messages, if any. + $( '.install-theme-info, [data-slug="' + args.slug + '"]' ).removeClass( 'theme-install-failed' ).find( '.notice.notice-error' ).remove(); + + $document.trigger( 'wp-theme-installing', args ); + + return wp.updates.ajax( 'install-theme', args ); + }; + + /** + * Updates the UI appropriately after a successful theme install. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the theme to be installed. + * @param {string} response.customizeUrl URL to the Customizer for the just installed theme. + * @param {string} response.activateUrl URL to activate the just installed theme. + */ + wp.updates.installThemeSuccess = function( response ) { + var $card = $( '.wp-full-overlay-header, [data-slug=' + response.slug + ']' ), + $message; + + $document.trigger( 'wp-theme-install-success', response ); + + $message = $card.find( '.button-primary' ) + .removeClass( 'updating-message' ) + .addClass( 'updated-message disabled' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Theme name and version. */ + _x( '%s installed!', 'theme' ), + response.themeName + ) + ) + .text( _x( 'Installed!', 'theme' ) ); + + wp.a11y.speak( __( 'Installation completed successfully.' ) ); + + setTimeout( function() { + + if ( response.activateUrl ) { + + // Transform the 'Install' button into an 'Activate' button. + $message + .attr( 'href', response.activateUrl ) + .removeClass( 'theme-install updated-message disabled' ) + .addClass( 'activate' ); + + if ( 'themes-network' === pagenow ) { + $message + .attr( + 'aria-label', + sprintf( + /* translators: %s: Theme name. */ + _x( 'Network Activate %s', 'theme' ), + response.themeName + ) + ) + .text( __( 'Network Enable' ) ); + } else { + $message + .attr( + 'aria-label', + sprintf( + /* translators: %s: Theme name. */ + _x( 'Activate %s', 'theme' ), + response.themeName + ) + ) + .text( __( 'Activate' ) ); + } + } + + if ( response.customizeUrl ) { + + // Transform the 'Preview' button into a 'Live Preview' button. + $message.siblings( '.preview' ).replaceWith( function () { + return $( '<a>' ) + .attr( 'href', response.customizeUrl ) + .addClass( 'button load-customize' ) + .text( __( 'Live Preview' ) ); + } ); + } + }, 1000 ); + }; + + /** + * Updates the UI appropriately after a failed theme install. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the theme to be installed. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.installThemeError = function( response ) { + var $card, $button, + errorMessage = sprintf( + /* translators: %s: Error string for a failed installation. */ + __( 'Installation failed: %s' ), + response.errorMessage + ), + $message = wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt', + message: errorMessage + } ); + + if ( ! wp.updates.isValidResponse( response, 'install' ) ) { + return; + } + + if ( wp.updates.maybeHandleCredentialError( response, 'install-theme' ) ) { + return; + } + + if ( 'customize' === pagenow ) { + if ( $document.find( 'body' ).hasClass( 'modal-open' ) ) { + $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); + $card = $( '.theme-overlay .theme-info' ).prepend( $message ); + } else { + $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); + $card = $button.closest( '.theme' ).addClass( 'theme-install-failed' ).append( $message ); + } + wp.customize.notifications.remove( 'theme_installing' ); + } else { + if ( $document.find( 'body' ).hasClass( 'full-overlay-active' ) ) { + $button = $( '.theme-install[data-slug="' + response.slug + '"]' ); + $card = $( '.install-theme-info' ).prepend( $message ); + } else { + $card = $( '[data-slug="' + response.slug + '"]' ).removeClass( 'focus' ).addClass( 'theme-install-failed' ).append( $message ); + $button = $card.find( '.theme-install' ); + } + } + + $button + .removeClass( 'updating-message' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Theme name and version. */ + _x( '%s installation failed', 'theme' ), + $button.data( 'name' ) + ) + ) + .text( __( 'Installation failed.' ) ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-theme-install-error', response ); + }; + + /** + * Sends an Ajax request to the server to delete a theme. + * + * @since 4.6.0 + * + * @param {Object} args + * @param {string} args.slug Theme stylesheet. + * @param {deleteThemeSuccess=} args.success Optional. Success callback. Default: wp.updates.deleteThemeSuccess + * @param {deleteThemeError=} args.error Optional. Error callback. Default: wp.updates.deleteThemeError + * @return {$.promise} A jQuery promise that represents the request, + * decorated with an abort() method. + */ + wp.updates.deleteTheme = function( args ) { + var $button; + + if ( 'themes' === pagenow ) { + $button = $( '.theme-actions .delete-theme' ); + } else if ( 'themes-network' === pagenow ) { + $button = $( '[data-slug="' + args.slug + '"]' ).find( '.row-actions a.delete' ); + } + + args = _.extend( { + success: wp.updates.deleteThemeSuccess, + error: wp.updates.deleteThemeError + }, args ); + + if ( $button && $button.html() !== __( 'Deleting...' ) ) { + $button + .data( 'originaltext', $button.html() ) + .text( __( 'Deleting...' ) ); + } + + wp.a11y.speak( __( 'Deleting...' ) ); + + // Remove previous error messages, if any. + $( '.theme-info .update-message' ).remove(); + + $document.trigger( 'wp-theme-deleting', args ); + + return wp.updates.ajax( 'delete-theme', args ); + }; + + /** + * Updates the UI appropriately after a successful theme deletion. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the theme that was deleted. + */ + wp.updates.deleteThemeSuccess = function( response ) { + var $themeRows = $( '[data-slug="' + response.slug + '"]' ); + + if ( 'themes-network' === pagenow ) { + + // Removes the theme and updates rows. + $themeRows.css( { backgroundColor: '#faafaa' } ).fadeOut( 350, function() { + var $views = $( '.subsubsub' ), + $themeRow = $( this ), + themes = settings.themes, + deletedRow = wp.template( 'item-deleted-row' ); + + if ( ! $themeRow.hasClass( 'plugin-update-tr' ) ) { + $themeRow.after( + deletedRow( { + slug: response.slug, + colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, + name: $themeRow.find( '.theme-title strong' ).text() + } ) + ); + } + + $themeRow.remove(); + + // Remove theme from update count. + if ( -1 !== _.indexOf( themes.upgrade, response.slug ) ) { + themes.upgrade = _.without( themes.upgrade, response.slug ); + wp.updates.decrementCount( 'theme' ); + } + + // Remove from views. + if ( -1 !== _.indexOf( themes.disabled, response.slug ) ) { + themes.disabled = _.without( themes.disabled, response.slug ); + if ( themes.disabled.length ) { + $views.find( '.disabled .count' ).text( '(' + themes.disabled.length + ')' ); + } else { + $views.find( '.disabled' ).remove(); + } + } + + if ( -1 !== _.indexOf( themes['auto-update-enabled'], response.slug ) ) { + themes['auto-update-enabled'] = _.without( themes['auto-update-enabled'], response.slug ); + if ( themes['auto-update-enabled'].length ) { + $views.find( '.auto-update-enabled .count' ).text( '(' + themes['auto-update-enabled'].length + ')' ); + } else { + $views.find( '.auto-update-enabled' ).remove(); + } + } + + if ( -1 !== _.indexOf( themes['auto-update-disabled'], response.slug ) ) { + themes['auto-update-disabled'] = _.without( themes['auto-update-disabled'], response.slug ); + if ( themes['auto-update-disabled'].length ) { + $views.find( '.auto-update-disabled .count' ).text( '(' + themes['auto-update-disabled'].length + ')' ); + } else { + $views.find( '.auto-update-disabled' ).remove(); + } + } + + themes.all = _.without( themes.all, response.slug ); + + // There is always at least one theme available. + $views.find( '.all .count' ).text( '(' + themes.all.length + ')' ); + } ); + } + + // DecrementCount from update count. + if ( 'themes' === pagenow ) { + var theme = _.find( _wpThemeSettings.themes, { id: response.slug } ); + if ( theme.hasUpdate ) { + wp.updates.decrementCount( 'theme' ); + } + } + + wp.a11y.speak( _x( 'Deleted!', 'theme' ) ); + + $document.trigger( 'wp-theme-delete-success', response ); + }; + + /** + * Updates the UI appropriately after a failed theme deletion. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.slug Slug of the theme to be deleted. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + */ + wp.updates.deleteThemeError = function( response ) { + var $themeRow = $( 'tr.inactive[data-slug="' + response.slug + '"]' ), + $button = $( '.theme-actions .delete-theme' ), + updateRow = wp.template( 'item-update-row' ), + $updateRow = $themeRow.siblings( '#' + response.slug + '-update' ), + errorMessage = sprintf( + /* translators: %s: Error string for a failed deletion. */ + __( 'Deletion failed: %s' ), + response.errorMessage + ), + $message = wp.updates.adminNotice( { + className: 'update-message notice-error notice-alt', + message: errorMessage + } ); + + if ( wp.updates.maybeHandleCredentialError( response, 'delete-theme' ) ) { + return; + } + + if ( 'themes-network' === pagenow ) { + if ( ! $updateRow.length ) { + $themeRow.addClass( 'update' ).after( + updateRow( { + slug: response.slug, + colspan: $( '#bulk-action-form' ).find( 'thead th:not(.hidden), thead td' ).length, + content: $message + } ) + ); + } else { + // Remove previous error messages, if any. + $updateRow.find( '.notice-error' ).remove(); + $updateRow.find( '.plugin-update' ).append( $message ); + } + } else { + $( '.theme-info .theme-description' ).before( $message ); + } + + $button.html( $button.data( 'originaltext' ) ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + $document.trigger( 'wp-theme-delete-error', response ); + }; + + /** + * Adds the appropriate callback based on the type of action and the current page. + * + * @since 4.6.0 + * @private + * + * @param {Object} data Ajax payload. + * @param {string} action The type of request to perform. + * @return {Object} The Ajax payload with the appropriate callbacks. + */ + wp.updates._addCallbacks = function( data, action ) { + if ( 'import' === pagenow && 'install-plugin' === action ) { + data.success = wp.updates.installImporterSuccess; + data.error = wp.updates.installImporterError; + } + + return data; + }; + + /** + * Pulls available jobs from the queue and runs them. + * + * @since 4.2.0 + * @since 4.6.0 Can handle multiple job types. + */ + wp.updates.queueChecker = function() { + var job; + + if ( wp.updates.ajaxLocked || ! wp.updates.queue.length ) { + return; + } + + job = wp.updates.queue.shift(); + + // Handle a queue job. + switch ( job.action ) { + case 'install-plugin': + wp.updates.installPlugin( job.data ); + break; + + case 'update-plugin': + wp.updates.updatePlugin( job.data ); + break; + + case 'delete-plugin': + wp.updates.deletePlugin( job.data ); + break; + + case 'install-theme': + wp.updates.installTheme( job.data ); + break; + + case 'update-theme': + wp.updates.updateTheme( job.data ); + break; + + case 'delete-theme': + wp.updates.deleteTheme( job.data ); + break; + + default: + break; + } + }; + + /** + * Requests the users filesystem credentials if they aren't already known. + * + * @since 4.2.0 + * + * @param {Event=} event Optional. Event interface. + */ + wp.updates.requestFilesystemCredentials = function( event ) { + if ( false === wp.updates.filesystemCredentials.available ) { + /* + * After exiting the credentials request modal, + * return the focus to the element triggering the request. + */ + if ( event && ! wp.updates.$elToReturnFocusToFromCredentialsModal ) { + wp.updates.$elToReturnFocusToFromCredentialsModal = $( event.target ); + } + + wp.updates.ajaxLocked = true; + wp.updates.requestForCredentialsModalOpen(); + } + }; + + /** + * Requests the users filesystem credentials if needed and there is no lock. + * + * @since 4.6.0 + * + * @param {Event=} event Optional. Event interface. + */ + wp.updates.maybeRequestFilesystemCredentials = function( event ) { + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + } + }; + + /** + * Keydown handler for the request for credentials modal. + * + * Closes the modal when the escape key is pressed and + * constrains keyboard navigation to inside the modal. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + wp.updates.keydown = function( event ) { + if ( 27 === event.keyCode ) { + wp.updates.requestForCredentialsModalCancel(); + } else if ( 9 === event.keyCode ) { + + // #upgrade button must always be the last focus-able element in the dialog. + if ( 'upgrade' === event.target.id && ! event.shiftKey ) { + $( '#hostname' ).trigger( 'focus' ); + + event.preventDefault(); + } else if ( 'hostname' === event.target.id && event.shiftKey ) { + $( '#upgrade' ).trigger( 'focus' ); + + event.preventDefault(); + } + } + }; + + /** + * Opens the request for credentials modal. + * + * @since 4.2.0 + */ + wp.updates.requestForCredentialsModalOpen = function() { + var $modal = $( '#request-filesystem-credentials-dialog' ); + + $( 'body' ).addClass( 'modal-open' ); + $modal.show(); + $modal.find( 'input:enabled:first' ).trigger( 'focus' ); + $modal.on( 'keydown', wp.updates.keydown ); + }; + + /** + * Closes the request for credentials modal. + * + * @since 4.2.0 + */ + wp.updates.requestForCredentialsModalClose = function() { + $( '#request-filesystem-credentials-dialog' ).hide(); + $( 'body' ).removeClass( 'modal-open' ); + + if ( wp.updates.$elToReturnFocusToFromCredentialsModal ) { + wp.updates.$elToReturnFocusToFromCredentialsModal.trigger( 'focus' ); + } + }; + + /** + * Takes care of the steps that need to happen when the modal is canceled out. + * + * @since 4.2.0 + * @since 4.6.0 Triggers an event for callbacks to listen to and add their actions. + */ + wp.updates.requestForCredentialsModalCancel = function() { + + // Not ajaxLocked and no queue means we already have cleared things up. + if ( ! wp.updates.ajaxLocked && ! wp.updates.queue.length ) { + return; + } + + _.each( wp.updates.queue, function( job ) { + $document.trigger( 'credential-modal-cancel', job ); + } ); + + // Remove the lock, and clear the queue. + wp.updates.ajaxLocked = false; + wp.updates.queue = []; + + wp.updates.requestForCredentialsModalClose(); + }; + + /** + * Displays an error message in the request for credentials form. + * + * @since 4.2.0 + * + * @param {string} message Error message. + */ + wp.updates.showErrorInCredentialsForm = function( message ) { + var $filesystemForm = $( '#request-filesystem-credentials-form' ); + + // Remove any existing error. + $filesystemForm.find( '.notice' ).remove(); + $filesystemForm.find( '#request-filesystem-credentials-title' ).after( '<div class="notice notice-alt notice-error"><p>' + message + '</p></div>' ); + }; + + /** + * Handles credential errors and runs events that need to happen in that case. + * + * @since 4.2.0 + * + * @param {Object} response Ajax response. + * @param {string} action The type of request to perform. + */ + wp.updates.credentialError = function( response, action ) { + + // Restore callbacks. + response = wp.updates._addCallbacks( response, action ); + + wp.updates.queue.unshift( { + action: action, + + /* + * Not cool that we're depending on response for this data. + * This would feel more whole in a view all tied together. + */ + data: response + } ); + + wp.updates.filesystemCredentials.available = false; + wp.updates.showErrorInCredentialsForm( response.errorMessage ); + wp.updates.requestFilesystemCredentials(); + }; + + /** + * Handles credentials errors if it could not connect to the filesystem. + * + * @since 4.6.0 + * + * @param {Object} response Response from the server. + * @param {string} response.errorCode Error code for the error that occurred. + * @param {string} response.errorMessage The error that occurred. + * @param {string} action The type of request to perform. + * @return {boolean} Whether there is an error that needs to be handled or not. + */ + wp.updates.maybeHandleCredentialError = function( response, action ) { + if ( wp.updates.shouldRequestFilesystemCredentials && response.errorCode && 'unable_to_connect_to_filesystem' === response.errorCode ) { + wp.updates.credentialError( response, action ); + return true; + } + + return false; + }; + + /** + * Validates an Ajax response to ensure it's a proper object. + * + * If the response deems to be invalid, an admin notice is being displayed. + * + * @param {(Object|string)} response Response from the server. + * @param {function=} response.always Optional. Callback for when the Deferred is resolved or rejected. + * @param {string=} response.statusText Optional. Status message corresponding to the status code. + * @param {string=} response.responseText Optional. Request response as text. + * @param {string} action Type of action the response is referring to. Can be 'delete', + * 'update' or 'install'. + */ + wp.updates.isValidResponse = function( response, action ) { + var error = __( 'Something went wrong.' ), + errorMessage; + + // Make sure the response is a valid data object and not a Promise object. + if ( _.isObject( response ) && ! _.isFunction( response.always ) ) { + return true; + } + + if ( _.isString( response ) && '-1' === response ) { + error = __( 'An error has occurred. Please reload the page and try again.' ); + } else if ( _.isString( response ) ) { + error = response; + } else if ( 'undefined' !== typeof response.readyState && 0 === response.readyState ) { + error = __( 'Connection lost or the server is busy. Please try again later.' ); + } else if ( _.isString( response.responseText ) && '' !== response.responseText ) { + error = response.responseText; + } else if ( _.isString( response.statusText ) ) { + error = response.statusText; + } + + switch ( action ) { + case 'update': + /* translators: %s: Error string for a failed update. */ + errorMessage = __( 'Update failed: %s' ); + break; + + case 'install': + /* translators: %s: Error string for a failed installation. */ + errorMessage = __( 'Installation failed: %s' ); + break; + + case 'delete': + /* translators: %s: Error string for a failed deletion. */ + errorMessage = __( 'Deletion failed: %s' ); + break; + } + + // Messages are escaped, remove HTML tags to make them more readable. + error = error.replace( /<[\/a-z][^<>]*>/gi, '' ); + errorMessage = errorMessage.replace( '%s', error ); + + // Add admin notice. + wp.updates.addAdminNotice( { + id: 'unknown_error', + className: 'notice-error is-dismissible', + message: _.escape( errorMessage ) + } ); + + // Remove the lock, and clear the queue. + wp.updates.ajaxLocked = false; + wp.updates.queue = []; + + // Change buttons of all running updates. + $( '.button.updating-message' ) + .removeClass( 'updating-message' ) + .removeAttr( 'aria-label' ) + .prop( 'disabled', true ) + .text( __( 'Update failed.' ) ); + + $( '.updating-message:not(.button):not(.thickbox)' ) + .removeClass( 'updating-message notice-warning' ) + .addClass( 'notice-error' ) + .find( 'p' ) + .removeAttr( 'aria-label' ) + .text( errorMessage ); + + wp.a11y.speak( errorMessage, 'assertive' ); + + return false; + }; + + /** + * Potentially adds an AYS to a user attempting to leave the page. + * + * If an update is on-going and a user attempts to leave the page, + * opens an "Are you sure?" alert. + * + * @since 4.2.0 + */ + wp.updates.beforeunload = function() { + if ( wp.updates.ajaxLocked ) { + return __( 'Updates may not complete if you navigate away from this page.' ); + } + }; + + $( function() { + var $pluginFilter = $( '#plugin-filter' ), + $bulkActionForm = $( '#bulk-action-form' ), + $filesystemForm = $( '#request-filesystem-credentials-form' ), + $filesystemModal = $( '#request-filesystem-credentials-dialog' ), + $pluginSearch = $( '.plugins-php .wp-filter-search' ), + $pluginInstallSearch = $( '.plugin-install-php .wp-filter-search' ); + + settings = _.extend( settings, window._wpUpdatesItemCounts || {} ); + + if ( settings.totals ) { + wp.updates.refreshCount(); + } + + /* + * Whether a user needs to submit filesystem credentials. + * + * This is based on whether the form was output on the page server-side. + * + * @see {wp_print_request_filesystem_credentials_modal() in PHP} + */ + wp.updates.shouldRequestFilesystemCredentials = $filesystemModal.length > 0; + + /** + * File system credentials form submit noop-er / handler. + * + * @since 4.2.0 + */ + $filesystemModal.on( 'submit', 'form', function( event ) { + event.preventDefault(); + + // Persist the credentials input by the user for the duration of the page load. + wp.updates.filesystemCredentials.ftp.hostname = $( '#hostname' ).val(); + wp.updates.filesystemCredentials.ftp.username = $( '#username' ).val(); + wp.updates.filesystemCredentials.ftp.password = $( '#password' ).val(); + wp.updates.filesystemCredentials.ftp.connectionType = $( 'input[name="connection_type"]:checked' ).val(); + wp.updates.filesystemCredentials.ssh.publicKey = $( '#public_key' ).val(); + wp.updates.filesystemCredentials.ssh.privateKey = $( '#private_key' ).val(); + wp.updates.filesystemCredentials.fsNonce = $( '#_fs_nonce' ).val(); + wp.updates.filesystemCredentials.available = true; + + // Unlock and invoke the queue. + wp.updates.ajaxLocked = false; + wp.updates.queueChecker(); + + wp.updates.requestForCredentialsModalClose(); + } ); + + /** + * Closes the request credentials modal when clicking the 'Cancel' button or outside of the modal. + * + * @since 4.2.0 + */ + $filesystemModal.on( 'click', '[data-js-action="close"], .notification-dialog-background', wp.updates.requestForCredentialsModalCancel ); + + /** + * Hide SSH fields when not selected. + * + * @since 4.2.0 + */ + $filesystemForm.on( 'change', 'input[name="connection_type"]', function() { + $( '#ssh-keys' ).toggleClass( 'hidden', ( 'ssh' !== $( this ).val() ) ); + } ).trigger( 'change' ); + + /** + * Handles events after the credential modal was closed. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + * @param {string} job The install/update.delete request. + */ + $document.on( 'credential-modal-cancel', function( event, job ) { + var $updatingMessage = $( '.updating-message' ), + $message, originalText; + + if ( 'import' === pagenow ) { + $updatingMessage.removeClass( 'updating-message' ); + } else if ( 'plugins' === pagenow || 'plugins-network' === pagenow ) { + if ( 'update-plugin' === job.action ) { + $message = $( 'tr[data-plugin="' + job.data.plugin + '"]' ).find( '.update-message' ); + } else if ( 'delete-plugin' === job.action ) { + $message = $( '[data-plugin="' + job.data.plugin + '"]' ).find( '.row-actions a.delete' ); + } + } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { + if ( 'update-theme' === job.action ) { + $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' ); + } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) { + $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' ); + } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) { + $message = $( '.theme-actions .delete-theme' ); + } + } else { + $message = $updatingMessage; + } + + if ( $message && $message.hasClass( 'updating-message' ) ) { + originalText = $message.data( 'originaltext' ); + + if ( 'undefined' === typeof originalText ) { + originalText = $( '<p>' ).html( $message.find( 'p' ).data( 'originaltext' ) ); + } + + $message + .removeClass( 'updating-message' ) + .html( originalText ); + + if ( 'plugin-install' === pagenow || 'plugin-install-network' === pagenow ) { + if ( 'update-plugin' === job.action ) { + $message.attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name and version. */ + _x( 'Update %s now', 'plugin' ), + $message.data( 'name' ) + ) + ); + } else if ( 'install-plugin' === job.action ) { + $message.attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name. */ + _x( 'Install %s now', 'plugin' ), + $message.data( 'name' ) + ) + ); + } + } + } + + wp.a11y.speak( __( 'Update canceled.' ) ); + } ); + + /** + * Click handler for plugin updates in List Table view. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[data-plugin] .update-link', function( event ) { + var $message = $( event.target ), + $pluginRow = $message.parents( 'tr' ); + + event.preventDefault(); + + if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + // Return the user to the input box of the plugin's table row after closing the modal. + wp.updates.$elToReturnFocusToFromCredentialsModal = $pluginRow.find( '.check-column input' ); + wp.updates.updatePlugin( { + plugin: $pluginRow.data( 'plugin' ), + slug: $pluginRow.data( 'slug' ) + } ); + } ); + + /** + * Click handler for plugin updates in plugin install view. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $pluginFilter.on( 'click', '.update-now', function( event ) { + var $button = $( event.target ); + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.updatePlugin( { + plugin: $button.data( 'plugin' ), + slug: $button.data( 'slug' ) + } ); + } ); + + /** + * Click handler for plugin installs in plugin install view. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $pluginFilter.on( 'click', '.install-now', function( event ) { + var $button = $( event.target ); + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) || $button.hasClass( 'button-disabled' ) ) { + return; + } + + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + + $document.on( 'credential-modal-cancel', function() { + var $message = $( '.install-now.updating-message' ); + + $message + .removeClass( 'updating-message' ) + .text( __( 'Install Now' ) ); + + wp.a11y.speak( __( 'Update canceled.' ) ); + } ); + } + + wp.updates.installPlugin( { + slug: $button.data( 'slug' ) + } ); + } ); + + /** + * Click handler for importer plugins installs in the Import screen. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.importer-item .install-now', function( event ) { + var $button = $( event.target ), + pluginName = $( this ).data( 'name' ); + + event.preventDefault(); + + if ( $button.hasClass( 'updating-message' ) ) { + return; + } + + if ( wp.updates.shouldRequestFilesystemCredentials && ! wp.updates.ajaxLocked ) { + wp.updates.requestFilesystemCredentials( event ); + + $document.on( 'credential-modal-cancel', function() { + + $button + .removeClass( 'updating-message' ) + .attr( + 'aria-label', + sprintf( + /* translators: %s: Plugin name. */ + _x( 'Install %s now', 'plugin' ), + pluginName + ) + ) + .text( __( 'Install Now' ) ); + + wp.a11y.speak( __( 'Update canceled.' ) ); + } ); + } + + wp.updates.installPlugin( { + slug: $button.data( 'slug' ), + pagenow: pagenow, + success: wp.updates.installImporterSuccess, + error: wp.updates.installImporterError + } ); + } ); + + /** + * Click handler for plugin deletions. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[data-plugin] a.delete', function( event ) { + var $pluginRow = $( event.target ).parents( 'tr' ), + confirmMessage; + + if ( $pluginRow.hasClass( 'is-uninstallable' ) ) { + confirmMessage = sprintf( + /* translators: %s: Plugin name. */ + __( 'Are you sure you want to delete %s and its data?' ), + $pluginRow.find( '.plugin-title strong' ).text() + ); + } else { + confirmMessage = sprintf( + /* translators: %s: Plugin name. */ + __( 'Are you sure you want to delete %s?' ), + $pluginRow.find( '.plugin-title strong' ).text() + ); + } + + event.preventDefault(); + + if ( ! window.confirm( confirmMessage ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.deletePlugin( { + plugin: $pluginRow.data( 'plugin' ), + slug: $pluginRow.data( 'slug' ) + } ); + + } ); + + /** + * Click handler for theme updates. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.themes-php.network-admin .update-link', function( event ) { + var $message = $( event.target ), + $themeRow = $message.parents( 'tr' ); + + event.preventDefault(); + + if ( $message.hasClass( 'updating-message' ) || $message.hasClass( 'button-disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + // Return the user to the input box of the theme's table row after closing the modal. + wp.updates.$elToReturnFocusToFromCredentialsModal = $themeRow.find( '.check-column input' ); + wp.updates.updateTheme( { + slug: $themeRow.data( 'slug' ) + } ); + } ); + + /** + * Click handler for theme deletions. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $document.on( 'click', '.themes-php.network-admin a.delete', function( event ) { + var $themeRow = $( event.target ).parents( 'tr' ), + confirmMessage = sprintf( + /* translators: %s: Theme name. */ + __( 'Are you sure you want to delete %s?' ), + $themeRow.find( '.theme-title strong' ).text() + ); + + event.preventDefault(); + + if ( ! window.confirm( confirmMessage ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + wp.updates.deleteTheme( { + slug: $themeRow.data( 'slug' ) + } ); + } ); + + /** + * Bulk action handler for plugins and themes. + * + * Handles both deletions and updates. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $bulkActionForm.on( 'click', '[type="submit"]:not([name="clear-recent-list"])', function( event ) { + var bulkAction = $( event.target ).siblings( 'select' ).val(), + itemsSelected = $bulkActionForm.find( 'input[name="checked[]"]:checked' ), + success = 0, + error = 0, + errorMessages = [], + type, action; + + // Determine which type of item we're dealing with. + switch ( pagenow ) { + case 'plugins': + case 'plugins-network': + type = 'plugin'; + break; + + case 'themes-network': + type = 'theme'; + break; + + default: + return; + } + + // Bail if there were no items selected. + if ( ! itemsSelected.length ) { + event.preventDefault(); + $( 'html, body' ).animate( { scrollTop: 0 } ); + + return wp.updates.addAdminNotice( { + id: 'no-items-selected', + className: 'notice-error is-dismissible', + message: __( 'Please select at least one item to perform this action on.' ) + } ); + } + + // Determine the type of request we're dealing with. + switch ( bulkAction ) { + case 'update-selected': + action = bulkAction.replace( 'selected', type ); + break; + + case 'delete-selected': + var confirmMessage = 'plugin' === type ? + __( 'Are you sure you want to delete the selected plugins and their data?' ) : + __( 'Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?' ); + + if ( ! window.confirm( confirmMessage ) ) { + event.preventDefault(); + return; + } + + action = bulkAction.replace( 'selected', type ); + break; + + default: + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + event.preventDefault(); + + // Un-check the bulk checkboxes. + $bulkActionForm.find( '.manage-column [type="checkbox"]' ).prop( 'checked', false ); + + $document.trigger( 'wp-' + type + '-bulk-' + bulkAction, itemsSelected ); + + // Find all the checkboxes which have been checked. + itemsSelected.each( function( index, element ) { + var $checkbox = $( element ), + $itemRow = $checkbox.parents( 'tr' ); + + // Only add update-able items to the update queue. + if ( 'update-selected' === bulkAction && ( ! $itemRow.hasClass( 'update' ) || $itemRow.find( 'notice-error' ).length ) ) { + + // Un-check the box. + $checkbox.prop( 'checked', false ); + return; + } + + // Don't add items to the update queue again, even if the user clicks the update button several times. + if ( 'update-selected' === bulkAction && $itemRow.hasClass( 'is-enqueued' ) ) { + return; + } + + $itemRow.addClass( 'is-enqueued' ); + + // Add it to the queue. + wp.updates.queue.push( { + action: action, + data: { + plugin: $itemRow.data( 'plugin' ), + slug: $itemRow.data( 'slug' ) + } + } ); + } ); + + // Display bulk notification for updates of any kind. + $document.on( 'wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error', function( event, response ) { + var $itemRow = $( '[data-slug="' + response.slug + '"]' ), + $bulkActionNotice, itemName; + + if ( 'wp-' + response.update + '-update-success' === event.type ) { + success++; + } else { + itemName = response.pluginName ? response.pluginName : $itemRow.find( '.column-primary strong' ).text(); + + error++; + errorMessages.push( itemName + ': ' + response.errorMessage ); + } + + $itemRow.find( 'input[name="checked[]"]:checked' ).prop( 'checked', false ); + + wp.updates.adminNotice = wp.template( 'wp-bulk-updates-admin-notice' ); + + wp.updates.addAdminNotice( { + id: 'bulk-action-notice', + className: 'bulk-action-notice', + successes: success, + errors: error, + errorMessages: errorMessages, + type: response.update + } ); + + $bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() { + // $( this ) is the clicked button, no need to get it again. + $( this ) + .toggleClass( 'bulk-action-errors-collapsed' ) + .attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) ); + // Show the errors list. + $bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' ); + } ); + + if ( error > 0 && ! wp.updates.queue.length ) { + $( 'html, body' ).animate( { scrollTop: 0 } ); + } + } ); + + // Reset admin notice template after #bulk-action-notice was added. + $document.on( 'wp-updates-notice-added', function() { + wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' ); + } ); + + // Check the queue, now that the event handlers have been added. + wp.updates.queueChecker(); + } ); + + if ( $pluginInstallSearch.length ) { + $pluginInstallSearch.attr( 'aria-describedby', 'live-search-desc' ); + } + + /** + * Handles changes to the plugin search box on the new-plugin page, + * searching the repository dynamically. + * + * @since 4.6.0 + */ + $pluginInstallSearch.on( 'keyup input', _.debounce( function( event, eventtype ) { + var $searchTab = $( '.plugin-install-search' ), data, searchLocation; + + data = { + _ajax_nonce: wp.updates.ajaxNonce, + s: encodeURIComponent( event.target.value ), + tab: 'search', + type: $( '#typeselector' ).val(), + pagenow: pagenow + }; + searchLocation = location.href.split( '?' )[ 0 ] + '?' + $.param( _.omit( data, [ '_ajax_nonce', 'pagenow' ] ) ); + + // Clear on escape. + if ( 'keyup' === event.type && 27 === event.which ) { + event.target.value = ''; + } + + if ( wp.updates.searchTerm === data.s && 'typechange' !== eventtype ) { + return; + } else { + $pluginFilter.empty(); + wp.updates.searchTerm = data.s; + } + + if ( window.history && window.history.replaceState ) { + window.history.replaceState( null, '', searchLocation ); + } + + if ( ! $searchTab.length ) { + $searchTab = $( '<li class="plugin-install-search" />' ) + .append( $( '<a />', { + 'class': 'current', + 'href': searchLocation, + 'text': __( 'Search Results' ) + } ) ); + + $( '.wp-filter .filter-links .current' ) + .removeClass( 'current' ) + .parents( '.filter-links' ) + .prepend( $searchTab ); + + $pluginFilter.prev( 'p' ).remove(); + $( '.plugins-popular-tags-wrapper' ).remove(); + } + + if ( 'undefined' !== typeof wp.updates.searchRequest ) { + wp.updates.searchRequest.abort(); + } + $( 'body' ).addClass( 'loading-content' ); + + wp.updates.searchRequest = wp.ajax.post( 'search-install-plugins', data ).done( function( response ) { + $( 'body' ).removeClass( 'loading-content' ); + $pluginFilter.append( response.items ); + delete wp.updates.searchRequest; + + if ( 0 === response.count ) { + wp.a11y.speak( __( 'You do not appear to have any plugins available at this time.' ) ); + } else { + wp.a11y.speak( + sprintf( + /* translators: %s: Number of plugins. */ + __( 'Number of plugins found: %d' ), + response.count + ) + ); + } + } ); + }, 1000 ) ); + + if ( $pluginSearch.length ) { + $pluginSearch.attr( 'aria-describedby', 'live-search-desc' ); + } + + /** + * Handles changes to the plugin search box on the Installed Plugins screen, + * searching the plugin list dynamically. + * + * @since 4.6.0 + */ + $pluginSearch.on( 'keyup input', _.debounce( function( event ) { + var data = { + _ajax_nonce: wp.updates.ajaxNonce, + s: encodeURIComponent( event.target.value ), + pagenow: pagenow, + plugin_status: 'all' + }, + queryArgs; + + // Clear on escape. + if ( 'keyup' === event.type && 27 === event.which ) { + event.target.value = ''; + } + + if ( wp.updates.searchTerm === data.s ) { + return; + } else { + wp.updates.searchTerm = data.s; + } + + queryArgs = _.object( _.compact( _.map( location.search.slice( 1 ).split( '&' ), function( item ) { + if ( item ) return item.split( '=' ); + } ) ) ); + + data.plugin_status = queryArgs.plugin_status || 'all'; + + if ( window.history && window.history.replaceState ) { + window.history.replaceState( null, '', location.href.split( '?' )[ 0 ] + '?s=' + data.s + '&plugin_status=' + data.plugin_status ); + } + + if ( 'undefined' !== typeof wp.updates.searchRequest ) { + wp.updates.searchRequest.abort(); + } + + $bulkActionForm.empty(); + $( 'body' ).addClass( 'loading-content' ); + $( '.subsubsub .current' ).removeClass( 'current' ); + + wp.updates.searchRequest = wp.ajax.post( 'search-plugins', data ).done( function( response ) { + + // Can we just ditch this whole subtitle business? + var $subTitle = $( '<span />' ).addClass( 'subtitle' ).html( + sprintf( + /* translators: %s: Search query. */ + __( 'Search results for: %s' ), + '<strong>' + _.escape( decodeURIComponent( data.s ) ) + '</strong>' + ) ), + $oldSubTitle = $( '.wrap .subtitle' ); + + if ( ! data.s.length ) { + $oldSubTitle.remove(); + $( '.subsubsub .' + data.plugin_status + ' a' ).addClass( 'current' ); + } else if ( $oldSubTitle.length ) { + $oldSubTitle.replaceWith( $subTitle ); + } else { + $( '.wp-header-end' ).before( $subTitle ); + } + + $( 'body' ).removeClass( 'loading-content' ); + $bulkActionForm.append( response.items ); + delete wp.updates.searchRequest; + + if ( 0 === response.count ) { + wp.a11y.speak( __( 'No plugins found. Try a different search.' ) ); + } else { + wp.a11y.speak( + sprintf( + /* translators: %s: Number of plugins. */ + __( 'Number of plugins found: %d' ), + response.count + ) + ); + } + } ); + }, 500 ) ); + + /** + * Trigger a search event when the search form gets submitted. + * + * @since 4.6.0 + */ + $document.on( 'submit', '.search-plugins', function( event ) { + event.preventDefault(); + + $( 'input.wp-filter-search' ).trigger( 'input' ); + } ); + + /** + * Trigger a search event when the "Try Again" button is clicked. + * + * @since 4.9.0 + */ + $document.on( 'click', '.try-again', function( event ) { + event.preventDefault(); + $pluginInstallSearch.trigger( 'input' ); + } ); + + /** + * Trigger a search event when the search type gets changed. + * + * @since 4.6.0 + */ + $( '#typeselector' ).on( 'change', function() { + var $search = $( 'input[name="s"]' ); + + if ( $search.val().length ) { + $search.trigger( 'input', 'typechange' ); + } + } ); + + /** + * Click handler for updating a plugin from the details modal on `plugin-install.php`. + * + * @since 4.2.0 + * + * @param {Event} event Event interface. + */ + $( '#plugin_update_from_iframe' ).on( 'click', function( event ) { + var target = window.parent === window ? null : window.parent, + update; + + $.support.postMessage = !! window.postMessage; + + if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'update-core.php' ) ) { + return; + } + + event.preventDefault(); + + update = { + action: 'update-plugin', + data: { + plugin: $( this ).data( 'plugin' ), + slug: $( this ).data( 'slug' ) + } + }; + + target.postMessage( JSON.stringify( update ), window.location.origin ); + } ); + + /** + * Click handler for installing a plugin from the details modal on `plugin-install.php`. + * + * @since 4.6.0 + * + * @param {Event} event Event interface. + */ + $( '#plugin_install_from_iframe' ).on( 'click', function( event ) { + var target = window.parent === window ? null : window.parent, + install; + + $.support.postMessage = !! window.postMessage; + + if ( false === $.support.postMessage || null === target || -1 !== window.parent.location.pathname.indexOf( 'index.php' ) ) { + return; + } + + event.preventDefault(); + + install = { + action: 'install-plugin', + data: { + slug: $( this ).data( 'slug' ) + } + }; + + target.postMessage( JSON.stringify( install ), window.location.origin ); + } ); + + /** + * Handles postMessage events. + * + * @since 4.2.0 + * @since 4.6.0 Switched `update-plugin` action to use the queue. + * + * @param {Event} event Event interface. + */ + $( window ).on( 'message', function( event ) { + var originalEvent = event.originalEvent, + expectedOrigin = document.location.protocol + '//' + document.location.host, + message; + + if ( originalEvent.origin !== expectedOrigin ) { + return; + } + + try { + message = JSON.parse( originalEvent.data ); + } catch ( e ) { + return; + } + + if ( ! message || 'undefined' === typeof message.action ) { + return; + } + + switch ( message.action ) { + + // Called from `wp-admin/includes/class-wp-upgrader-skins.php`. + case 'decrementUpdateCount': + /** @property {string} message.upgradeType */ + wp.updates.decrementCount( message.upgradeType ); + break; + + case 'install-plugin': + case 'update-plugin': + /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ + window.tb_remove(); + /* jscs:enable */ + + message.data = wp.updates._addCallbacks( message.data, message.action ); + + wp.updates.queue.push( message ); + wp.updates.queueChecker(); + break; + } + } ); + + /** + * Adds a callback to display a warning before leaving the page. + * + * @since 4.2.0 + */ + $( window ).on( 'beforeunload', wp.updates.beforeunload ); + + /** + * Prevents the page form scrolling when activating auto-updates with the Spacebar key. + * + * @since 5.5.0 + */ + $document.on( 'keydown', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { + if ( 32 === event.which ) { + event.preventDefault(); + } + } ); + + /** + * Click and keyup handler for enabling and disabling plugin and theme auto-updates. + * + * These controls can be either links or buttons. When JavaScript is enabled, + * we want them to behave like buttons. An ARIA role `button` is added via + * the JavaScript that targets elements with the CSS class `aria-button-if-js`. + * + * @since 5.5.0 + */ + $document.on( 'click keyup', '.column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update', function( event ) { + var data, asset, type, $parent, + $toggler = $( this ), + action = $toggler.attr( 'data-wp-action' ), + $label = $toggler.find( '.label' ); + + if ( 'keyup' === event.type && 32 !== event.which ) { + return; + } + + if ( 'themes' !== pagenow ) { + $parent = $toggler.closest( '.column-auto-updates' ); + } else { + $parent = $toggler.closest( '.theme-autoupdate' ); + } + + event.preventDefault(); + + // Prevent multiple simultaneous requests. + if ( $toggler.attr( 'data-doing-ajax' ) === 'yes' ) { + return; + } + + $toggler.attr( 'data-doing-ajax', 'yes' ); + + switch ( pagenow ) { + case 'plugins': + case 'plugins-network': + type = 'plugin'; + asset = $toggler.closest( 'tr' ).attr( 'data-plugin' ); + break; + case 'themes-network': + type = 'theme'; + asset = $toggler.closest( 'tr' ).attr( 'data-slug' ); + break; + case 'themes': + type = 'theme'; + asset = $toggler.attr( 'data-slug' ); + break; + } + + // Clear any previous errors. + $parent.find( '.notice.notice-error' ).addClass( 'hidden' ); + + // Show loading status. + if ( 'enable' === action ) { + $label.text( __( 'Enabling...' ) ); + } else { + $label.text( __( 'Disabling...' ) ); + } + + $toggler.find( '.dashicons-update' ).removeClass( 'hidden' ); + + data = { + action: 'toggle-auto-updates', + _ajax_nonce: settings.ajax_nonce, + state: action, + type: type, + asset: asset + }; + + $.post( window.ajaxurl, data ) + .done( function( response ) { + var $enabled, $disabled, enabledNumber, disabledNumber, errorMessage, + href = $toggler.attr( 'href' ); + + if ( ! response.success ) { + // if WP returns 0 for response (which can happen in a few cases), + // output the general error message since we won't have response.data.error. + if ( response.data && response.data.error ) { + errorMessage = response.data.error; + } else { + errorMessage = __( 'The request could not be completed.' ); + } + + $parent.find( '.notice.notice-error' ).removeClass( 'hidden' ).find( 'p' ).text( errorMessage ); + wp.a11y.speak( errorMessage, 'assertive' ); + return; + } + + // Update the counts in the enabled/disabled views if on a screen + // with a list table. + if ( 'themes' !== pagenow ) { + $enabled = $( '.auto-update-enabled span' ); + $disabled = $( '.auto-update-disabled span' ); + enabledNumber = parseInt( $enabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; + disabledNumber = parseInt( $disabled.text().replace( /[^\d]+/g, '' ), 10 ) || 0; + + switch ( action ) { + case 'enable': + ++enabledNumber; + --disabledNumber; + break; + case 'disable': + --enabledNumber; + ++disabledNumber; + break; + } + + enabledNumber = Math.max( 0, enabledNumber ); + disabledNumber = Math.max( 0, disabledNumber ); + + $enabled.text( '(' + enabledNumber + ')' ); + $disabled.text( '(' + disabledNumber + ')' ); + } + + if ( 'enable' === action ) { + // The toggler control can be either a link or a button. + if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { + href = href.replace( 'action=enable-auto-update', 'action=disable-auto-update' ); + $toggler.attr( 'href', href ); + } + $toggler.attr( 'data-wp-action', 'disable' ); + + $label.text( __( 'Disable auto-updates' ) ); + $parent.find( '.auto-update-time' ).removeClass( 'hidden' ); + wp.a11y.speak( __( 'Auto-updates enabled' ) ); + } else { + // The toggler control can be either a link or a button. + if ( $toggler[ 0 ].hasAttribute( 'href' ) ) { + href = href.replace( 'action=disable-auto-update', 'action=enable-auto-update' ); + $toggler.attr( 'href', href ); + } + $toggler.attr( 'data-wp-action', 'enable' ); + + $label.text( __( 'Enable auto-updates' ) ); + $parent.find( '.auto-update-time' ).addClass( 'hidden' ); + wp.a11y.speak( __( 'Auto-updates disabled' ) ); + } + + $document.trigger( 'wp-auto-update-setting-changed', { state: action, type: type, asset: asset } ); + } ) + .fail( function() { + $parent.find( '.notice.notice-error' ) + .removeClass( 'hidden' ) + .find( 'p' ) + .text( __( 'The request could not be completed.' ) ); + + wp.a11y.speak( __( 'The request could not be completed.' ), 'assertive' ); + } ) + .always( function() { + $toggler.removeAttr( 'data-doing-ajax' ).find( '.dashicons-update' ).addClass( 'hidden' ); + } ); + } + ); + } ); +})( jQuery, window.wp, window._wpUpdatesSettings ); diff --git a/wp-admin/js/updates.min.js b/wp-admin/js/updates.min.js new file mode 100644 index 0000000..1c1bfbb --- /dev/null +++ b/wp-admin/js/updates.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(c,g,m){var h=c(document),f=g.i18n.__,i=g.i18n._x,l=g.i18n._n,o=g.i18n._nx,r=g.i18n.sprintf;(g=g||{}).updates={},g.updates.l10n={searchResults:"",searchResultsLabel:"",noPlugins:"",noItemsSelected:"",updating:"",pluginUpdated:"",themeUpdated:"",update:"",updateNow:"",pluginUpdateNowLabel:"",updateFailedShort:"",updateFailed:"",pluginUpdatingLabel:"",pluginUpdatedLabel:"",pluginUpdateFailedLabel:"",updatingMsg:"",updatedMsg:"",updateCancel:"",beforeunload:"",installNow:"",pluginInstallNowLabel:"",installing:"",pluginInstalled:"",themeInstalled:"",installFailedShort:"",installFailed:"",pluginInstallingLabel:"",themeInstallingLabel:"",pluginInstalledLabel:"",themeInstalledLabel:"",pluginInstallFailedLabel:"",themeInstallFailedLabel:"",installingMsg:"",installedMsg:"",importerInstalledMsg:"",aysDelete:"",aysDeleteUninstall:"",aysBulkDelete:"",aysBulkDeleteThemes:"",deleting:"",deleteFailed:"",pluginDeleted:"",themeDeleted:"",livePreview:"",activatePlugin:"",activateTheme:"",activatePluginLabel:"",activateThemeLabel:"",activateImporter:"",activateImporterLabel:"",unknownError:"",connectionError:"",nonceError:"",pluginsFound:"",noPluginsFound:"",autoUpdatesEnable:"",autoUpdatesEnabling:"",autoUpdatesEnabled:"",autoUpdatesDisable:"",autoUpdatesDisabling:"",autoUpdatesDisabled:"",autoUpdatesError:""},g.updates.l10n=window.wp.deprecateL10nObject("wp.updates.l10n",g.updates.l10n,"5.5.0"),g.updates.ajaxNonce=m.ajax_nonce,g.updates.searchTerm="",g.updates.shouldRequestFilesystemCredentials=!1,g.updates.filesystemCredentials={ftp:{host:"",username:"",password:"",connectionType:""},ssh:{publicKey:"",privateKey:""},fsNonce:"",available:!1},g.updates.ajaxLocked=!1,g.updates.adminNotice=g.template("wp-updates-admin-notice"),g.updates.queue=[],g.updates.$elToReturnFocusToFromCredentialsModal=void 0,g.updates.addAdminNotice=function(e){var t,a=c(e.selector),s=c(".wp-header-end");delete e.selector,t=g.updates.adminNotice(e),(a=a.length?a:c("#"+e.id)).length?a.replaceWith(t):s.length?s.after(t):"customize"===pagenow?c(".customize-themes-notifications").append(t):c(".wrap").find("> h1").after(t),h.trigger("wp-updates-notice-added")},g.updates.ajax=function(e,t){var a={};return g.updates.ajaxLocked?(g.updates.queue.push({action:e,data:t}),c.Deferred()):(g.updates.ajaxLocked=!0,t.success&&(a.success=t.success,delete t.success),t.error&&(a.error=t.error,delete t.error),a.data=_.extend(t,{action:e,_ajax_nonce:g.updates.ajaxNonce,_fs_nonce:g.updates.filesystemCredentials.fsNonce,username:g.updates.filesystemCredentials.ftp.username,password:g.updates.filesystemCredentials.ftp.password,hostname:g.updates.filesystemCredentials.ftp.hostname,connection_type:g.updates.filesystemCredentials.ftp.connectionType,public_key:g.updates.filesystemCredentials.ssh.publicKey,private_key:g.updates.filesystemCredentials.ssh.privateKey}),g.ajax.send(a).always(g.updates.ajaxAlways))},g.updates.ajaxAlways=function(e){e.errorCode&&"unable_to_connect_to_filesystem"===e.errorCode||(g.updates.ajaxLocked=!1,g.updates.queueChecker()),void 0!==e.debug&&window.console&&window.console.log&&_.map(e.debug,function(e){window.console.log(g.sanitize.stripTagsAndEncodeText(e))})},g.updates.refreshCount=function(){var e,t=c("#wp-admin-bar-updates"),a=c('a[href="update-core.php"] .update-plugins'),s=c('a[href="plugins.php"] .update-plugins'),n=c('a[href="themes.php"] .update-plugins');t.find(".ab-label").text(m.totals.counts.total),t.find(".updates-available-text").text(r(l("%s update available","%s updates available",m.totals.counts.total),m.totals.counts.total)),0===m.totals.counts.total&&t.find(".ab-label").parents("li").remove(),a.each(function(e,t){t.className=t.className.replace(/count-\d+/,"count-"+m.totals.counts.total)}),0<m.totals.counts.total?a.find(".update-count").text(m.totals.counts.total):a.remove(),s.each(function(e,t){t.className=t.className.replace(/count-\d+/,"count-"+m.totals.counts.plugins)}),0<m.totals.counts.total?s.find(".plugin-count").text(m.totals.counts.plugins):s.remove(),n.each(function(e,t){t.className=t.className.replace(/count-\d+/,"count-"+m.totals.counts.themes)}),0<m.totals.counts.total?n.find(".theme-count").text(m.totals.counts.themes):n.remove(),"plugins"===pagenow||"plugins-network"===pagenow?e=m.totals.counts.plugins:"themes"!==pagenow&&"themes-network"!==pagenow||(e=m.totals.counts.themes),0<e?c(".subsubsub .upgrade .count").text("("+e+")"):(c(".subsubsub .upgrade").remove(),c(".subsubsub li:last").html(function(){return c(this).children()}))},g.updates.decrementCount=function(e){m.totals.counts.total=Math.max(--m.totals.counts.total,0),"plugin"===e?m.totals.counts.plugins=Math.max(--m.totals.counts.plugins,0):"theme"===e&&(m.totals.counts.themes=Math.max(--m.totals.counts.themes,0)),g.updates.refreshCount(e)},g.updates.updatePlugin=function(e){var t,a,s,n=c("#wp-admin-bar-updates");return e=_.extend({success:g.updates.updatePluginSuccess,error:g.updates.updatePluginError},e),"plugins"===pagenow||"plugins-network"===pagenow?(a=(s=c('tr[data-plugin="'+e.plugin+'"]')).find(".update-message").removeClass("notice-error").addClass("updating-message notice-warning").find("p"),s=r(i("Updating %s...","plugin"),s.find(".plugin-title strong").text())):"plugin-install"!==pagenow&&"plugin-install-network"!==pagenow||(a=(t=c(".plugin-card-"+e.slug)).find(".update-now").addClass("updating-message"),s=r(i("Updating %s...","plugin"),a.data("name")),t.removeClass("plugin-card-update-failed").find(".notice.notice-error").remove()),n.addClass("spin"),a.html()!==f("Updating...")&&a.data("originaltext",a.html()),a.attr("aria-label",s).text(f("Updating...")),h.trigger("wp-plugin-updating",e),g.updates.ajax("update-plugin",e)},g.updates.updatePluginSuccess=function(e){var t,a,s,n=c("#wp-admin-bar-updates");"plugins"===pagenow||"plugins-network"===pagenow?(a=(t=c('tr[data-plugin="'+e.plugin+'"]').removeClass("update is-enqueued").addClass("updated")).find(".update-message").removeClass("updating-message notice-warning").addClass("updated-message notice-success").find("p"),s=t.find(".plugin-version-author-uri").html().replace(e.oldVersion,e.newVersion),t.find(".plugin-version-author-uri").html(s),t.find(".auto-update-time").empty()):"plugin-install"!==pagenow&&"plugin-install-network"!==pagenow||(a=c(".plugin-card-"+e.slug).find(".update-now").removeClass("updating-message").addClass("button-disabled updated-message")),n.removeClass("spin"),a.attr("aria-label",r(i("%s updated!","plugin"),e.pluginName)).text(i("Updated!","plugin")),g.a11y.speak(f("Update completed successfully.")),g.updates.decrementCount("plugin"),h.trigger("wp-plugin-update-success",e)},g.updates.updatePluginError=function(e){var t,a,s,n=c("#wp-admin-bar-updates");g.updates.isValidResponse(e,"update")&&!g.updates.maybeHandleCredentialError(e,"update-plugin")&&(s=r(f("Update failed: %s"),e.errorMessage),"plugins"===pagenow||"plugins-network"===pagenow?(c('tr[data-plugin="'+e.plugin+'"]').removeClass("is-enqueued"),(a=(e.plugin?c('tr[data-plugin="'+e.plugin+'"]'):c('tr[data-slug="'+e.slug+'"]')).find(".update-message")).removeClass("updating-message notice-warning").addClass("notice-error").find("p").html(s),e.pluginName?a.find("p").attr("aria-label",r(i("%s update failed.","plugin"),e.pluginName)):a.find("p").removeAttr("aria-label")):"plugin-install"!==pagenow&&"plugin-install-network"!==pagenow||((t=c(".plugin-card-"+e.slug).addClass("plugin-card-update-failed").append(g.updates.adminNotice({className:"update-message notice-error notice-alt is-dismissible",message:s}))).find(".update-now").text(f("Update failed.")).removeClass("updating-message"),e.pluginName?t.find(".update-now").attr("aria-label",r(i("%s update failed.","plugin"),e.pluginName)):t.find(".update-now").removeAttr("aria-label"),t.on("click",".notice.is-dismissible .notice-dismiss",function(){setTimeout(function(){t.removeClass("plugin-card-update-failed").find(".column-name a").trigger("focus"),t.find(".update-now").attr("aria-label",!1).text(f("Update Now"))},200)})),n.removeClass("spin"),g.a11y.speak(s,"assertive"),h.trigger("wp-plugin-update-error",e))},g.updates.installPlugin=function(e){var t=c(".plugin-card-"+e.slug),a=t.find(".install-now");return e=_.extend({success:g.updates.installPluginSuccess,error:g.updates.installPluginError},e),(a="import"===pagenow?c('[data-slug="'+e.slug+'"]'):a).html()!==f("Installing...")&&a.data("originaltext",a.html()),a.addClass("updating-message").attr("aria-label",r(i("Installing %s...","plugin"),a.data("name"))).text(f("Installing...")),g.a11y.speak(f("Installing... please wait.")),t.removeClass("plugin-card-install-failed").find(".notice.notice-error").remove(),h.trigger("wp-plugin-installing",e),g.updates.ajax("install-plugin",e)},g.updates.installPluginSuccess=function(e){var t=c(".plugin-card-"+e.slug).find(".install-now");t.removeClass("updating-message").addClass("updated-message installed button-disabled").attr("aria-label",r(i("%s installed!","plugin"),e.pluginName)).text(i("Installed!","plugin")),g.a11y.speak(f("Installation completed successfully.")),h.trigger("wp-plugin-install-success",e),e.activateUrl&&setTimeout(function(){t.removeClass("install-now installed button-disabled updated-message").addClass("activate-now button-primary").attr("href",e.activateUrl),"plugins-network"===pagenow?t.attr("aria-label",r(i("Network Activate %s","plugin"),e.pluginName)).text(f("Network Activate")):t.attr("aria-label",r(i("Activate %s","plugin"),e.pluginName)).text(f("Activate"))},1e3)},g.updates.installPluginError=function(e){var t,a=c(".plugin-card-"+e.slug),s=a.find(".install-now");g.updates.isValidResponse(e,"install")&&!g.updates.maybeHandleCredentialError(e,"install-plugin")&&(t=r(f("Installation failed: %s"),e.errorMessage),a.addClass("plugin-card-update-failed").append('<div class="notice notice-error notice-alt is-dismissible"><p>'+t+"</p></div>"),a.on("click",".notice.is-dismissible .notice-dismiss",function(){setTimeout(function(){a.removeClass("plugin-card-update-failed").find(".column-name a").trigger("focus")},200)}),s.removeClass("updating-message").addClass("button-disabled").attr("aria-label",r(i("%s installation failed","plugin"),s.data("name"))).text(f("Installation failed.")),g.a11y.speak(t,"assertive"),h.trigger("wp-plugin-install-error",e))},g.updates.installImporterSuccess=function(e){g.updates.addAdminNotice({id:"install-success",className:"notice-success is-dismissible",message:r(f('Importer installed successfully. <a href="%s">Run importer</a>'),e.activateUrl+"&from=import")}),c('[data-slug="'+e.slug+'"]').removeClass("install-now updating-message").addClass("activate-now").attr({href:e.activateUrl+"&from=import","aria-label":r(f("Run %s"),e.pluginName)}).text(f("Run Importer")),g.a11y.speak(f("Installation completed successfully.")),h.trigger("wp-importer-install-success",e)},g.updates.installImporterError=function(e){var t=r(f("Installation failed: %s"),e.errorMessage),a=c('[data-slug="'+e.slug+'"]'),s=a.data("name");g.updates.isValidResponse(e,"install")&&!g.updates.maybeHandleCredentialError(e,"install-plugin")&&(g.updates.addAdminNotice({id:e.errorCode,className:"notice-error is-dismissible",message:t}),a.removeClass("updating-message").attr("aria-label",r(i("Install %s now","plugin"),s)).text(f("Install Now")),g.a11y.speak(t,"assertive"),h.trigger("wp-importer-install-error",e))},g.updates.deletePlugin=function(e){var t=c('[data-plugin="'+e.plugin+'"]').find(".row-actions a.delete");return e=_.extend({success:g.updates.deletePluginSuccess,error:g.updates.deletePluginError},e),t.html()!==f("Deleting...")&&t.data("originaltext",t.html()).text(f("Deleting...")),g.a11y.speak(f("Deleting...")),h.trigger("wp-plugin-deleting",e),g.updates.ajax("delete-plugin",e)},g.updates.deletePluginSuccess=function(u){c('[data-plugin="'+u.plugin+'"]').css({backgroundColor:"#faafaa"}).fadeOut(350,function(){var e=c("#bulk-action-form"),t=c(".subsubsub"),a=c(this),s=t.find('[aria-current="page"]'),n=c(".displaying-num"),l=e.find("thead th:not(.hidden), thead td").length,i=g.template("item-deleted-row"),d=m.plugins;a.hasClass("plugin-update-tr")||a.after(i({slug:u.slug,plugin:u.plugin,colspan:l,name:u.pluginName})),a.remove(),-1!==_.indexOf(d.upgrade,u.plugin)&&(d.upgrade=_.without(d.upgrade,u.plugin),g.updates.decrementCount("plugin")),-1!==_.indexOf(d.inactive,u.plugin)&&(d.inactive=_.without(d.inactive,u.plugin),d.inactive.length?t.find(".inactive .count").text("("+d.inactive.length+")"):t.find(".inactive").remove()),-1!==_.indexOf(d.active,u.plugin)&&(d.active=_.without(d.active,u.plugin),d.active.length?t.find(".active .count").text("("+d.active.length+")"):t.find(".active").remove()),-1!==_.indexOf(d.recently_activated,u.plugin)&&(d.recently_activated=_.without(d.recently_activated,u.plugin),d.recently_activated.length?t.find(".recently_activated .count").text("("+d.recently_activated.length+")"):t.find(".recently_activated").remove()),-1!==_.indexOf(d["auto-update-enabled"],u.plugin)&&(d["auto-update-enabled"]=_.without(d["auto-update-enabled"],u.plugin),d["auto-update-enabled"].length?t.find(".auto-update-enabled .count").text("("+d["auto-update-enabled"].length+")"):t.find(".auto-update-enabled").remove()),-1!==_.indexOf(d["auto-update-disabled"],u.plugin)&&(d["auto-update-disabled"]=_.without(d["auto-update-disabled"],u.plugin),d["auto-update-disabled"].length?t.find(".auto-update-disabled .count").text("("+d["auto-update-disabled"].length+")"):t.find(".auto-update-disabled").remove()),d.all=_.without(d.all,u.plugin),d.all.length?t.find(".all .count").text("("+d.all.length+")"):(e.find(".tablenav").css({visibility:"hidden"}),t.find(".all").remove(),e.find("tr.no-items").length||e.find("#the-list").append('<tr class="no-items"><td class="colspanchange" colspan="'+l+'">'+f("No plugins are currently available.")+"</td></tr>")),n.length&&s.length&&(i=d[s.parent("li").attr("class")].length,n.text(r(o("%s item","%s items","plugin/plugins",i),i)))}),g.a11y.speak(i("Deleted!","plugin")),h.trigger("wp-plugin-delete-success",u)},g.updates.deletePluginError=function(e){var t,a=g.template("item-update-row"),s=g.updates.adminNotice({className:"update-message notice-error notice-alt",message:e.errorMessage}),n=e.plugin?(t=c('tr.inactive[data-plugin="'+e.plugin+'"]')).siblings('[data-plugin="'+e.plugin+'"]'):(t=c('tr.inactive[data-slug="'+e.slug+'"]')).siblings('[data-slug="'+e.slug+'"]');g.updates.isValidResponse(e,"delete")&&!g.updates.maybeHandleCredentialError(e,"delete-plugin")&&(n.length?(n.find(".notice-error").remove(),n.find(".plugin-update").append(s)):t.addClass("update").after(a({slug:e.slug,plugin:e.plugin||e.slug,colspan:c("#bulk-action-form").find("thead th:not(.hidden), thead td").length,content:s})),h.trigger("wp-plugin-delete-error",e))},g.updates.updateTheme=function(e){var t;return e=_.extend({success:g.updates.updateThemeSuccess,error:g.updates.updateThemeError},e),(t=("themes-network"===pagenow?c('[data-slug="'+e.slug+'"]').find(".update-message").removeClass("notice-error").addClass("updating-message notice-warning"):(t="customize"===pagenow?((t=c('[data-slug="'+e.slug+'"].notice').removeClass("notice-large")).find("h3").remove(),t.add(c("#customize-control-installed_theme_"+e.slug).find(".update-message"))):((t=c("#update-theme").closest(".notice").removeClass("notice-large")).find("h3").remove(),t.add(c('[data-slug="'+e.slug+'"]').find(".update-message")))).addClass("updating-message")).find("p")).html()!==f("Updating...")&&t.data("originaltext",t.html()),g.a11y.speak(f("Updating... please wait.")),t.text(f("Updating...")),h.trigger("wp-theme-updating",e),g.updates.ajax("update-theme",e)},g.updates.updateThemeSuccess=function(e){var t,a,s=c("body.modal-open").length,n=c('[data-slug="'+e.slug+'"]'),l={className:"updated-message notice-success notice-alt",message:i("Updated!","theme")};"customize"===pagenow?((n=c(".updating-message").siblings(".theme-name")).length&&(a=n.html().replace(e.oldVersion,e.newVersion),n.html(a)),t=c(".theme-info .notice").add(g.customize.control("installed_theme_"+e.slug).container.find(".theme").find(".update-message"))):"themes-network"===pagenow?(t=n.find(".update-message"),a=n.find(".theme-version-author-uri").html().replace(e.oldVersion,e.newVersion),n.find(".theme-version-author-uri").html(a),n.find(".auto-update-time").empty()):(t=c(".theme-info .notice").add(n.find(".update-message")),s?(c(".load-customize:visible").trigger("focus"),c(".theme-info .theme-autoupdate").find(".auto-update-time").empty()):n.find(".load-customize").trigger("focus")),g.updates.addAdminNotice(_.extend({selector:t},l)),g.a11y.speak(f("Update completed successfully.")),g.updates.decrementCount("theme"),h.trigger("wp-theme-update-success",e),s&&"customize"!==pagenow&&c(".theme-info .theme-author").after(g.updates.adminNotice(l))},g.updates.updateThemeError=function(e){var t,a=c('[data-slug="'+e.slug+'"]'),s=r(f("Update failed: %s"),e.errorMessage);g.updates.isValidResponse(e,"update")&&!g.updates.maybeHandleCredentialError(e,"update-theme")&&("customize"===pagenow&&(a=g.customize.control("installed_theme_"+e.slug).container.find(".theme")),"themes-network"===pagenow?t=a.find(".update-message "):(t=c(".theme-info .notice").add(a.find(".notice")),(c("body.modal-open").length?c(".load-customize:visible"):a.find(".load-customize")).trigger("focus")),g.updates.addAdminNotice({selector:t,className:"update-message notice-error notice-alt is-dismissible",message:s}),g.a11y.speak(s),h.trigger("wp-theme-update-error",e))},g.updates.installTheme=function(e){var t=c('.theme-install[data-slug="'+e.slug+'"]');return e=_.extend({success:g.updates.installThemeSuccess,error:g.updates.installThemeError},e),t.addClass("updating-message"),t.parents(".theme").addClass("focus"),t.html()!==f("Installing...")&&t.data("originaltext",t.html()),t.attr("aria-label",r(i("Installing %s...","theme"),t.data("name"))).text(f("Installing...")),g.a11y.speak(f("Installing... please wait.")),c('.install-theme-info, [data-slug="'+e.slug+'"]').removeClass("theme-install-failed").find(".notice.notice-error").remove(),h.trigger("wp-theme-installing",e),g.updates.ajax("install-theme",e)},g.updates.installThemeSuccess=function(e){var t,a=c(".wp-full-overlay-header, [data-slug="+e.slug+"]");h.trigger("wp-theme-install-success",e),t=a.find(".button-primary").removeClass("updating-message").addClass("updated-message disabled").attr("aria-label",r(i("%s installed!","theme"),e.themeName)).text(i("Installed!","theme")),g.a11y.speak(f("Installation completed successfully.")),setTimeout(function(){e.activateUrl&&(t.attr("href",e.activateUrl).removeClass("theme-install updated-message disabled").addClass("activate"),"themes-network"===pagenow?t.attr("aria-label",r(i("Network Activate %s","theme"),e.themeName)).text(f("Network Enable")):t.attr("aria-label",r(i("Activate %s","theme"),e.themeName)).text(f("Activate"))),e.customizeUrl&&t.siblings(".preview").replaceWith(function(){return c("<a>").attr("href",e.customizeUrl).addClass("button load-customize").text(f("Live Preview"))})},1e3)},g.updates.installThemeError=function(e){var t,a=r(f("Installation failed: %s"),e.errorMessage),s=g.updates.adminNotice({className:"update-message notice-error notice-alt",message:a});g.updates.isValidResponse(e,"install")&&!g.updates.maybeHandleCredentialError(e,"install-theme")&&("customize"===pagenow?(h.find("body").hasClass("modal-open")?(t=c('.theme-install[data-slug="'+e.slug+'"]'),c(".theme-overlay .theme-info").prepend(s)):(t=c('.theme-install[data-slug="'+e.slug+'"]')).closest(".theme").addClass("theme-install-failed").append(s),g.customize.notifications.remove("theme_installing")):h.find("body").hasClass("full-overlay-active")?(t=c('.theme-install[data-slug="'+e.slug+'"]'),c(".install-theme-info").prepend(s)):t=c('[data-slug="'+e.slug+'"]').removeClass("focus").addClass("theme-install-failed").append(s).find(".theme-install"),t.removeClass("updating-message").attr("aria-label",r(i("%s installation failed","theme"),t.data("name"))).text(f("Installation failed.")),g.a11y.speak(a,"assertive"),h.trigger("wp-theme-install-error",e))},g.updates.deleteTheme=function(e){var t;return"themes"===pagenow?t=c(".theme-actions .delete-theme"):"themes-network"===pagenow&&(t=c('[data-slug="'+e.slug+'"]').find(".row-actions a.delete")),e=_.extend({success:g.updates.deleteThemeSuccess,error:g.updates.deleteThemeError},e),t&&t.html()!==f("Deleting...")&&t.data("originaltext",t.html()).text(f("Deleting...")),g.a11y.speak(f("Deleting...")),c(".theme-info .update-message").remove(),h.trigger("wp-theme-deleting",e),g.updates.ajax("delete-theme",e)},g.updates.deleteThemeSuccess=function(n){var e=c('[data-slug="'+n.slug+'"]');"themes-network"===pagenow&&e.css({backgroundColor:"#faafaa"}).fadeOut(350,function(){var e=c(".subsubsub"),t=c(this),a=m.themes,s=g.template("item-deleted-row");t.hasClass("plugin-update-tr")||t.after(s({slug:n.slug,colspan:c("#bulk-action-form").find("thead th:not(.hidden), thead td").length,name:t.find(".theme-title strong").text()})),t.remove(),-1!==_.indexOf(a.upgrade,n.slug)&&(a.upgrade=_.without(a.upgrade,n.slug),g.updates.decrementCount("theme")),-1!==_.indexOf(a.disabled,n.slug)&&(a.disabled=_.without(a.disabled,n.slug),a.disabled.length?e.find(".disabled .count").text("("+a.disabled.length+")"):e.find(".disabled").remove()),-1!==_.indexOf(a["auto-update-enabled"],n.slug)&&(a["auto-update-enabled"]=_.without(a["auto-update-enabled"],n.slug),a["auto-update-enabled"].length?e.find(".auto-update-enabled .count").text("("+a["auto-update-enabled"].length+")"):e.find(".auto-update-enabled").remove()),-1!==_.indexOf(a["auto-update-disabled"],n.slug)&&(a["auto-update-disabled"]=_.without(a["auto-update-disabled"],n.slug),a["auto-update-disabled"].length?e.find(".auto-update-disabled .count").text("("+a["auto-update-disabled"].length+")"):e.find(".auto-update-disabled").remove()),a.all=_.without(a.all,n.slug),e.find(".all .count").text("("+a.all.length+")")}),"themes"===pagenow&&_.find(_wpThemeSettings.themes,{id:n.slug}).hasUpdate&&g.updates.decrementCount("theme"),g.a11y.speak(i("Deleted!","theme")),h.trigger("wp-theme-delete-success",n)},g.updates.deleteThemeError=function(e){var t=c('tr.inactive[data-slug="'+e.slug+'"]'),a=c(".theme-actions .delete-theme"),s=g.template("item-update-row"),n=t.siblings("#"+e.slug+"-update"),l=r(f("Deletion failed: %s"),e.errorMessage),i=g.updates.adminNotice({className:"update-message notice-error notice-alt",message:l});g.updates.maybeHandleCredentialError(e,"delete-theme")||("themes-network"===pagenow?n.length?(n.find(".notice-error").remove(),n.find(".plugin-update").append(i)):t.addClass("update").after(s({slug:e.slug,colspan:c("#bulk-action-form").find("thead th:not(.hidden), thead td").length,content:i})):c(".theme-info .theme-description").before(i),a.html(a.data("originaltext")),g.a11y.speak(l,"assertive"),h.trigger("wp-theme-delete-error",e))},g.updates._addCallbacks=function(e,t){return"import"===pagenow&&"install-plugin"===t&&(e.success=g.updates.installImporterSuccess,e.error=g.updates.installImporterError),e},g.updates.queueChecker=function(){var e;if(!g.updates.ajaxLocked&&g.updates.queue.length)switch((e=g.updates.queue.shift()).action){case"install-plugin":g.updates.installPlugin(e.data);break;case"update-plugin":g.updates.updatePlugin(e.data);break;case"delete-plugin":g.updates.deletePlugin(e.data);break;case"install-theme":g.updates.installTheme(e.data);break;case"update-theme":g.updates.updateTheme(e.data);break;case"delete-theme":g.updates.deleteTheme(e.data)}},g.updates.requestFilesystemCredentials=function(e){!1===g.updates.filesystemCredentials.available&&(e&&!g.updates.$elToReturnFocusToFromCredentialsModal&&(g.updates.$elToReturnFocusToFromCredentialsModal=c(e.target)),g.updates.ajaxLocked=!0,g.updates.requestForCredentialsModalOpen())},g.updates.maybeRequestFilesystemCredentials=function(e){g.updates.shouldRequestFilesystemCredentials&&!g.updates.ajaxLocked&&g.updates.requestFilesystemCredentials(e)},g.updates.keydown=function(e){27===e.keyCode?g.updates.requestForCredentialsModalCancel():9===e.keyCode&&("upgrade"!==e.target.id||e.shiftKey?"hostname"===e.target.id&&e.shiftKey&&(c("#upgrade").trigger("focus"),e.preventDefault()):(c("#hostname").trigger("focus"),e.preventDefault()))},g.updates.requestForCredentialsModalOpen=function(){var e=c("#request-filesystem-credentials-dialog");c("body").addClass("modal-open"),e.show(),e.find("input:enabled:first").trigger("focus"),e.on("keydown",g.updates.keydown)},g.updates.requestForCredentialsModalClose=function(){c("#request-filesystem-credentials-dialog").hide(),c("body").removeClass("modal-open"),g.updates.$elToReturnFocusToFromCredentialsModal&&g.updates.$elToReturnFocusToFromCredentialsModal.trigger("focus")},g.updates.requestForCredentialsModalCancel=function(){(g.updates.ajaxLocked||g.updates.queue.length)&&(_.each(g.updates.queue,function(e){h.trigger("credential-modal-cancel",e)}),g.updates.ajaxLocked=!1,g.updates.queue=[],g.updates.requestForCredentialsModalClose())},g.updates.showErrorInCredentialsForm=function(e){var t=c("#request-filesystem-credentials-form");t.find(".notice").remove(),t.find("#request-filesystem-credentials-title").after('<div class="notice notice-alt notice-error"><p>'+e+"</p></div>")},g.updates.credentialError=function(e,t){e=g.updates._addCallbacks(e,t),g.updates.queue.unshift({action:t,data:e}),g.updates.filesystemCredentials.available=!1,g.updates.showErrorInCredentialsForm(e.errorMessage),g.updates.requestFilesystemCredentials()},g.updates.maybeHandleCredentialError=function(e,t){return!(!g.updates.shouldRequestFilesystemCredentials||!e.errorCode||"unable_to_connect_to_filesystem"!==e.errorCode||(g.updates.credentialError(e,t),0))},g.updates.isValidResponse=function(e,t){var a,s=f("Something went wrong.");if(_.isObject(e)&&!_.isFunction(e.always))return!0;switch(_.isString(e)&&"-1"===e?s=f("An error has occurred. Please reload the page and try again."):_.isString(e)?s=e:void 0!==e.readyState&&0===e.readyState?s=f("Connection lost or the server is busy. Please try again later."):_.isString(e.responseText)&&""!==e.responseText?s=e.responseText:_.isString(e.statusText)&&(s=e.statusText),t){case"update":a=f("Update failed: %s");break;case"install":a=f("Installation failed: %s");break;case"delete":a=f("Deletion failed: %s")}return s=s.replace(/<[\/a-z][^<>]*>/gi,""),a=a.replace("%s",s),g.updates.addAdminNotice({id:"unknown_error",className:"notice-error is-dismissible",message:_.escape(a)}),g.updates.ajaxLocked=!1,g.updates.queue=[],c(".button.updating-message").removeClass("updating-message").removeAttr("aria-label").prop("disabled",!0).text(f("Update failed.")),c(".updating-message:not(.button):not(.thickbox)").removeClass("updating-message notice-warning").addClass("notice-error").find("p").removeAttr("aria-label").text(a),g.a11y.speak(a,"assertive"),!1},g.updates.beforeunload=function(){if(g.updates.ajaxLocked)return f("Updates may not complete if you navigate away from this page.")},c(function(){var l=c("#plugin-filter"),o=c("#bulk-action-form"),e=c("#request-filesystem-credentials-form"),t=c("#request-filesystem-credentials-dialog"),a=c(".plugins-php .wp-filter-search"),s=c(".plugin-install-php .wp-filter-search");(m=_.extend(m,window._wpUpdatesItemCounts||{})).totals&&g.updates.refreshCount(),g.updates.shouldRequestFilesystemCredentials=0<t.length,t.on("submit","form",function(e){e.preventDefault(),g.updates.filesystemCredentials.ftp.hostname=c("#hostname").val(),g.updates.filesystemCredentials.ftp.username=c("#username").val(),g.updates.filesystemCredentials.ftp.password=c("#password").val(),g.updates.filesystemCredentials.ftp.connectionType=c('input[name="connection_type"]:checked').val(),g.updates.filesystemCredentials.ssh.publicKey=c("#public_key").val(),g.updates.filesystemCredentials.ssh.privateKey=c("#private_key").val(),g.updates.filesystemCredentials.fsNonce=c("#_fs_nonce").val(),g.updates.filesystemCredentials.available=!0,g.updates.ajaxLocked=!1,g.updates.queueChecker(),g.updates.requestForCredentialsModalClose()}),t.on("click",'[data-js-action="close"], .notification-dialog-background',g.updates.requestForCredentialsModalCancel),e.on("change",'input[name="connection_type"]',function(){c("#ssh-keys").toggleClass("hidden","ssh"!==c(this).val())}).trigger("change"),h.on("credential-modal-cancel",function(e,t){var a,s=c(".updating-message");"import"===pagenow?s.removeClass("updating-message"):"plugins"===pagenow||"plugins-network"===pagenow?"update-plugin"===t.action?a=c('tr[data-plugin="'+t.data.plugin+'"]').find(".update-message"):"delete-plugin"===t.action&&(a=c('[data-plugin="'+t.data.plugin+'"]').find(".row-actions a.delete")):"themes"===pagenow||"themes-network"===pagenow?"update-theme"===t.action?a=c('[data-slug="'+t.data.slug+'"]').find(".update-message"):"delete-theme"===t.action&&"themes-network"===pagenow?a=c('[data-slug="'+t.data.slug+'"]').find(".row-actions a.delete"):"delete-theme"===t.action&&"themes"===pagenow&&(a=c(".theme-actions .delete-theme")):a=s,a&&a.hasClass("updating-message")&&(void 0===(s=a.data("originaltext"))&&(s=c("<p>").html(a.find("p").data("originaltext"))),a.removeClass("updating-message").html(s),"plugin-install"!==pagenow&&"plugin-install-network"!==pagenow||("update-plugin"===t.action?a.attr("aria-label",r(i("Update %s now","plugin"),a.data("name"))):"install-plugin"===t.action&&a.attr("aria-label",r(i("Install %s now","plugin"),a.data("name"))))),g.a11y.speak(f("Update canceled."))}),o.on("click","[data-plugin] .update-link",function(e){var t=c(e.target),a=t.parents("tr");e.preventDefault(),t.hasClass("updating-message")||t.hasClass("button-disabled")||(g.updates.maybeRequestFilesystemCredentials(e),g.updates.$elToReturnFocusToFromCredentialsModal=a.find(".check-column input"),g.updates.updatePlugin({plugin:a.data("plugin"),slug:a.data("slug")}))}),l.on("click",".update-now",function(e){var t=c(e.target);e.preventDefault(),t.hasClass("updating-message")||t.hasClass("button-disabled")||(g.updates.maybeRequestFilesystemCredentials(e),g.updates.updatePlugin({plugin:t.data("plugin"),slug:t.data("slug")}))}),l.on("click",".install-now",function(e){var t=c(e.target);e.preventDefault(),t.hasClass("updating-message")||t.hasClass("button-disabled")||(g.updates.shouldRequestFilesystemCredentials&&!g.updates.ajaxLocked&&(g.updates.requestFilesystemCredentials(e),h.on("credential-modal-cancel",function(){c(".install-now.updating-message").removeClass("updating-message").text(f("Install Now")),g.a11y.speak(f("Update canceled."))})),g.updates.installPlugin({slug:t.data("slug")}))}),h.on("click",".importer-item .install-now",function(e){var t=c(e.target),a=c(this).data("name");e.preventDefault(),t.hasClass("updating-message")||(g.updates.shouldRequestFilesystemCredentials&&!g.updates.ajaxLocked&&(g.updates.requestFilesystemCredentials(e),h.on("credential-modal-cancel",function(){t.removeClass("updating-message").attr("aria-label",r(i("Install %s now","plugin"),a)).text(f("Install Now")),g.a11y.speak(f("Update canceled."))})),g.updates.installPlugin({slug:t.data("slug"),pagenow:pagenow,success:g.updates.installImporterSuccess,error:g.updates.installImporterError}))}),o.on("click","[data-plugin] a.delete",function(e){var t=c(e.target).parents("tr"),a=t.hasClass("is-uninstallable")?r(f("Are you sure you want to delete %s and its data?"),t.find(".plugin-title strong").text()):r(f("Are you sure you want to delete %s?"),t.find(".plugin-title strong").text());e.preventDefault(),window.confirm(a)&&(g.updates.maybeRequestFilesystemCredentials(e),g.updates.deletePlugin({plugin:t.data("plugin"),slug:t.data("slug")}))}),h.on("click",".themes-php.network-admin .update-link",function(e){var t=c(e.target),a=t.parents("tr");e.preventDefault(),t.hasClass("updating-message")||t.hasClass("button-disabled")||(g.updates.maybeRequestFilesystemCredentials(e),g.updates.$elToReturnFocusToFromCredentialsModal=a.find(".check-column input"),g.updates.updateTheme({slug:a.data("slug")}))}),h.on("click",".themes-php.network-admin a.delete",function(e){var t=c(e.target).parents("tr"),a=r(f("Are you sure you want to delete %s?"),t.find(".theme-title strong").text());e.preventDefault(),window.confirm(a)&&(g.updates.maybeRequestFilesystemCredentials(e),g.updates.deleteTheme({slug:t.data("slug")}))}),o.on("click",'[type="submit"]:not([name="clear-recent-list"])',function(e){var t,s,n=c(e.target).siblings("select").val(),a=o.find('input[name="checked[]"]:checked'),l=0,i=0,d=[];switch(pagenow){case"plugins":case"plugins-network":t="plugin";break;case"themes-network":t="theme";break;default:return}if(!a.length)return e.preventDefault(),c("html, body").animate({scrollTop:0}),g.updates.addAdminNotice({id:"no-items-selected",className:"notice-error is-dismissible",message:f("Please select at least one item to perform this action on.")});switch(n){case"update-selected":s=n.replace("selected",t);break;case"delete-selected":var u=f("plugin"===t?"Are you sure you want to delete the selected plugins and their data?":"Caution: These themes may be active on other sites in the network. Are you sure you want to proceed?");if(!window.confirm(u))return void e.preventDefault();s=n.replace("selected",t);break;default:return}g.updates.maybeRequestFilesystemCredentials(e),e.preventDefault(),o.find('.manage-column [type="checkbox"]').prop("checked",!1),h.trigger("wp-"+t+"-bulk-"+n,a),a.each(function(e,t){var t=c(t),a=t.parents("tr");"update-selected"!==n||a.hasClass("update")&&!a.find("notice-error").length?"update-selected"===n&&a.hasClass("is-enqueued")||(a.addClass("is-enqueued"),g.updates.queue.push({action:s,data:{plugin:a.data("plugin"),slug:a.data("slug")}})):t.prop("checked",!1)}),h.on("wp-plugin-update-success wp-plugin-update-error wp-theme-update-success wp-theme-update-error",function(e,t){var a,s=c('[data-slug="'+t.slug+'"]');"wp-"+t.update+"-update-success"===e.type?l++:(e=t.pluginName||s.find(".column-primary strong").text(),i++,d.push(e+": "+t.errorMessage)),s.find('input[name="checked[]"]:checked').prop("checked",!1),g.updates.adminNotice=g.template("wp-bulk-updates-admin-notice"),g.updates.addAdminNotice({id:"bulk-action-notice",className:"bulk-action-notice",successes:l,errors:i,errorMessages:d,type:t.update}),a=c("#bulk-action-notice").on("click","button",function(){c(this).toggleClass("bulk-action-errors-collapsed").attr("aria-expanded",!c(this).hasClass("bulk-action-errors-collapsed")),a.find(".bulk-action-errors").toggleClass("hidden")}),0<i&&!g.updates.queue.length&&c("html, body").animate({scrollTop:0})}),h.on("wp-updates-notice-added",function(){g.updates.adminNotice=g.template("wp-updates-admin-notice")}),g.updates.queueChecker()}),s.length&&s.attr("aria-describedby","live-search-desc"),s.on("keyup input",_.debounce(function(e,t){var a=c(".plugin-install-search"),s={_ajax_nonce:g.updates.ajaxNonce,s:encodeURIComponent(e.target.value),tab:"search",type:c("#typeselector").val(),pagenow:pagenow},n=location.href.split("?")[0]+"?"+c.param(_.omit(s,["_ajax_nonce","pagenow"]));"keyup"===e.type&&27===e.which&&(e.target.value=""),g.updates.searchTerm===s.s&&"typechange"!==t||(l.empty(),g.updates.searchTerm=s.s,window.history&&window.history.replaceState&&window.history.replaceState(null,"",n),a.length||(a=c('<li class="plugin-install-search" />').append(c("<a />",{class:"current",href:n,text:f("Search Results")})),c(".wp-filter .filter-links .current").removeClass("current").parents(".filter-links").prepend(a),l.prev("p").remove(),c(".plugins-popular-tags-wrapper").remove()),void 0!==g.updates.searchRequest&&g.updates.searchRequest.abort(),c("body").addClass("loading-content"),g.updates.searchRequest=g.ajax.post("search-install-plugins",s).done(function(e){c("body").removeClass("loading-content"),l.append(e.items),delete g.updates.searchRequest,0===e.count?g.a11y.speak(f("You do not appear to have any plugins available at this time.")):g.a11y.speak(r(f("Number of plugins found: %d"),e.count))}))},1e3)),a.length&&a.attr("aria-describedby","live-search-desc"),a.on("keyup input",_.debounce(function(e){var s={_ajax_nonce:g.updates.ajaxNonce,s:encodeURIComponent(e.target.value),pagenow:pagenow,plugin_status:"all"};"keyup"===e.type&&27===e.which&&(e.target.value=""),g.updates.searchTerm!==s.s&&(g.updates.searchTerm=s.s,e=_.object(_.compact(_.map(location.search.slice(1).split("&"),function(e){if(e)return e.split("=")}))),s.plugin_status=e.plugin_status||"all",window.history&&window.history.replaceState&&window.history.replaceState(null,"",location.href.split("?")[0]+"?s="+s.s+"&plugin_status="+s.plugin_status),void 0!==g.updates.searchRequest&&g.updates.searchRequest.abort(),o.empty(),c("body").addClass("loading-content"),c(".subsubsub .current").removeClass("current"),g.updates.searchRequest=g.ajax.post("search-plugins",s).done(function(e){var t=c("<span />").addClass("subtitle").html(r(f("Search results for: %s"),"<strong>"+_.escape(decodeURIComponent(s.s))+"</strong>")),a=c(".wrap .subtitle");s.s.length?a.length?a.replaceWith(t):c(".wp-header-end").before(t):(a.remove(),c(".subsubsub ."+s.plugin_status+" a").addClass("current")),c("body").removeClass("loading-content"),o.append(e.items),delete g.updates.searchRequest,0===e.count?g.a11y.speak(f("No plugins found. Try a different search.")):g.a11y.speak(r(f("Number of plugins found: %d"),e.count))}))},500)),h.on("submit",".search-plugins",function(e){e.preventDefault(),c("input.wp-filter-search").trigger("input")}),h.on("click",".try-again",function(e){e.preventDefault(),s.trigger("input")}),c("#typeselector").on("change",function(){var e=c('input[name="s"]');e.val().length&&e.trigger("input","typechange")}),c("#plugin_update_from_iframe").on("click",function(e){var t=window.parent===window?null:window.parent;c.support.postMessage=!!window.postMessage,!1!==c.support.postMessage&&null!==t&&-1===window.parent.location.pathname.indexOf("update-core.php")&&(e.preventDefault(),e={action:"update-plugin",data:{plugin:c(this).data("plugin"),slug:c(this).data("slug")}},t.postMessage(JSON.stringify(e),window.location.origin))}),c("#plugin_install_from_iframe").on("click",function(e){var t=window.parent===window?null:window.parent;c.support.postMessage=!!window.postMessage,!1!==c.support.postMessage&&null!==t&&-1===window.parent.location.pathname.indexOf("index.php")&&(e.preventDefault(),e={action:"install-plugin",data:{slug:c(this).data("slug")}},t.postMessage(JSON.stringify(e),window.location.origin))}),c(window).on("message",function(e){var t,e=e.originalEvent,a=document.location.protocol+"//"+document.location.host;if(e.origin===a){try{t=JSON.parse(e.data)}catch(e){return}if(t&&void 0!==t.action)switch(t.action){case"decrementUpdateCount":g.updates.decrementCount(t.upgradeType);break;case"install-plugin":case"update-plugin":window.tb_remove(),t.data=g.updates._addCallbacks(t.data,t.action),g.updates.queue.push(t),g.updates.queueChecker()}}}),c(window).on("beforeunload",g.updates.beforeunload),h.on("keydown",".column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update",function(e){32===e.which&&e.preventDefault()}),h.on("click keyup",".column-auto-updates .toggle-auto-update, .theme-overlay .toggle-auto-update",function(e){var i,d,u,o=c(this),r=o.attr("data-wp-action"),p=o.find(".label");if(("keyup"!==e.type||32===e.which)&&(u="themes"!==pagenow?o.closest(".column-auto-updates"):o.closest(".theme-autoupdate"),e.preventDefault(),"yes"!==o.attr("data-doing-ajax"))){switch(o.attr("data-doing-ajax","yes"),pagenow){case"plugins":case"plugins-network":d="plugin",i=o.closest("tr").attr("data-plugin");break;case"themes-network":d="theme",i=o.closest("tr").attr("data-slug");break;case"themes":d="theme",i=o.attr("data-slug")}u.find(".notice.notice-error").addClass("hidden"),"enable"===r?p.text(f("Enabling...")):p.text(f("Disabling...")),o.find(".dashicons-update").removeClass("hidden"),e={action:"toggle-auto-updates",_ajax_nonce:m.ajax_nonce,state:r,type:d,asset:i},c.post(window.ajaxurl,e).done(function(e){var t,a,s,n,l=o.attr("href");if(e.success){if("themes"!==pagenow){switch(n=c(".auto-update-enabled span"),t=c(".auto-update-disabled span"),a=parseInt(n.text().replace(/[^\d]+/g,""),10)||0,s=parseInt(t.text().replace(/[^\d]+/g,""),10)||0,r){case"enable":++a,--s;break;case"disable":--a,++s}a=Math.max(0,a),s=Math.max(0,s),n.text("("+a+")"),t.text("("+s+")")}"enable"===r?(o[0].hasAttribute("href")&&(l=l.replace("action=enable-auto-update","action=disable-auto-update"),o.attr("href",l)),o.attr("data-wp-action","disable"),p.text(f("Disable auto-updates")),u.find(".auto-update-time").removeClass("hidden"),g.a11y.speak(f("Auto-updates enabled"))):(o[0].hasAttribute("href")&&(l=l.replace("action=disable-auto-update","action=enable-auto-update"),o.attr("href",l)),o.attr("data-wp-action","enable"),p.text(f("Enable auto-updates")),u.find(".auto-update-time").addClass("hidden"),g.a11y.speak(f("Auto-updates disabled"))),h.trigger("wp-auto-update-setting-changed",{state:r,type:d,asset:i})}else n=e.data&&e.data.error?e.data.error:f("The request could not be completed."),u.find(".notice.notice-error").removeClass("hidden").find("p").text(n),g.a11y.speak(n,"assertive")}).fail(function(){u.find(".notice.notice-error").removeClass("hidden").find("p").text(f("The request could not be completed.")),g.a11y.speak(f("The request could not be completed."),"assertive")}).always(function(){o.removeAttr("data-doing-ajax").find(".dashicons-update").addClass("hidden")})}})})}(jQuery,window.wp,window._wpUpdatesSettings);
\ No newline at end of file diff --git a/wp-admin/js/user-profile.js b/wp-admin/js/user-profile.js new file mode 100644 index 0000000..466d115 --- /dev/null +++ b/wp-admin/js/user-profile.js @@ -0,0 +1,497 @@ +/** + * @output wp-admin/js/user-profile.js + */ + +/* global ajaxurl, pwsL10n, userProfileL10n */ +(function($) { + var updateLock = false, + __ = wp.i18n.__, + $pass1Row, + $pass1, + $pass2, + $weakRow, + $weakCheckbox, + $toggleButton, + $submitButtons, + $submitButton, + currentPass, + $passwordWrapper; + + function generatePassword() { + if ( typeof zxcvbn !== 'function' ) { + setTimeout( generatePassword, 50 ); + return; + } else if ( ! $pass1.val() || $passwordWrapper.hasClass( 'is-open' ) ) { + // zxcvbn loaded before user entered password, or generating new password. + $pass1.val( $pass1.data( 'pw' ) ); + $pass1.trigger( 'pwupdate' ); + showOrHideWeakPasswordCheckbox(); + } else { + // zxcvbn loaded after the user entered password, check strength. + check_pass_strength(); + showOrHideWeakPasswordCheckbox(); + } + + /* + * This works around a race condition when zxcvbn loads quickly and + * causes `generatePassword()` to run prior to the toggle button being + * bound. + */ + bindToggleButton(); + + // Install screen. + if ( 1 !== parseInt( $toggleButton.data( 'start-masked' ), 10 ) ) { + // Show the password not masked if admin_password hasn't been posted yet. + $pass1.attr( 'type', 'text' ); + } else { + // Otherwise, mask the password. + $toggleButton.trigger( 'click' ); + } + + // Once zxcvbn loads, passwords strength is known. + $( '#pw-weak-text-label' ).text( __( 'Confirm use of weak password' ) ); + + // Focus the password field. + if ( 'mailserver_pass' !== $pass1.prop('id' ) ) { + $( $pass1 ).trigger( 'focus' ); + } + } + + function bindPass1() { + currentPass = $pass1.val(); + + if ( 1 === parseInt( $pass1.data( 'reveal' ), 10 ) ) { + generatePassword(); + } + + $pass1.on( 'input' + ' pwupdate', function () { + if ( $pass1.val() === currentPass ) { + return; + } + + currentPass = $pass1.val(); + + // Refresh password strength area. + $pass1.removeClass( 'short bad good strong' ); + showOrHideWeakPasswordCheckbox(); + } ); + } + + function resetToggle( show ) { + $toggleButton + .attr({ + 'aria-label': show ? __( 'Show password' ) : __( 'Hide password' ) + }) + .find( '.text' ) + .text( show ? __( 'Show' ) : __( 'Hide' ) ) + .end() + .find( '.dashicons' ) + .removeClass( show ? 'dashicons-hidden' : 'dashicons-visibility' ) + .addClass( show ? 'dashicons-visibility' : 'dashicons-hidden' ); + } + + function bindToggleButton() { + if ( !! $toggleButton ) { + // Do not rebind. + return; + } + $toggleButton = $pass1Row.find('.wp-hide-pw'); + $toggleButton.show().on( 'click', function () { + if ( 'password' === $pass1.attr( 'type' ) ) { + $pass1.attr( 'type', 'text' ); + resetToggle( false ); + } else { + $pass1.attr( 'type', 'password' ); + resetToggle( true ); + } + }); + } + + /** + * Handle the password reset button. Sets up an ajax callback to trigger sending + * a password reset email. + */ + function bindPasswordResetLink() { + $( '#generate-reset-link' ).on( 'click', function() { + var $this = $(this), + data = { + 'user_id': userProfileL10n.user_id, // The user to send a reset to. + 'nonce': userProfileL10n.nonce // Nonce to validate the action. + }; + + // Remove any previous error messages. + $this.parent().find( '.notice-error' ).remove(); + + // Send the reset request. + var resetAction = wp.ajax.post( 'send-password-reset', data ); + + // Handle reset success. + resetAction.done( function( response ) { + addInlineNotice( $this, true, response ); + } ); + + // Handle reset failure. + resetAction.fail( function( response ) { + addInlineNotice( $this, false, response ); + } ); + + }); + + } + + /** + * Helper function to insert an inline notice of success or failure. + * + * @param {jQuery Object} $this The button element: the message will be inserted + * above this button + * @param {bool} success Whether the message is a success message. + * @param {string} message The message to insert. + */ + function addInlineNotice( $this, success, message ) { + var resultDiv = $( '<div />' ); + + // 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( '<p />'); + + // 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(); + } + } + } + } + + $( 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); + $('<option />', { + 'text': val + }).appendTo( select ); + } + }); + }); + + /** + * Replaces "Howdy, *" in the admin toolbar whenever the display name dropdown is updated for one's own profile. + */ + select.on( 'change', function() { + if ( user_id !== current_user_id ) { + return; + } + + var display_name = this.value.trim() || current_name; + + greeting.text( display_name ); + } ); + } + + $colorpicker = $( '#color-picker' ); + $stylesheet = $( '#colors-css' ); + user_id = $( 'input#user_id' ).val(); + current_user_id = $( 'input[name="checkuser_id"]' ).val(); + + $colorpicker.on( 'click.colorpicker', '.color-option', function() { + var colors, + $this = $(this); + + if ( $this.hasClass( 'selected' ) ) { + return; + } + + $this.siblings( '.selected' ).removeClass( 'selected' ); + $this.addClass( 'selected' ).find( 'input[type="radio"]' ).prop( 'checked', true ); + + // Set color scheme. + if ( user_id === current_user_id ) { + // Load the colors stylesheet. + // The default color scheme won't have one, so we'll need to create an element. + if ( 0 === $stylesheet.length ) { + $stylesheet = $( '<link rel="stylesheet" />' ).appendTo( 'head' ); + } + $stylesheet.attr( 'href', $this.children( '.css_url' ).val() ); + + // Repaint icons. + if ( typeof wp !== 'undefined' && wp.svgPainter ) { + try { + colors = JSON.parse( $this.children( '.icon_colors' ).val() ); + } catch ( error ) {} + + if ( colors ) { + wp.svgPainter.setColors( colors ); + wp.svgPainter.paint(); + } + } + + // Update user option. + $.post( ajaxurl, { + action: 'save-user-color-scheme', + color_scheme: $this.children( 'input[name="admin_color"]' ).val(), + nonce: $('#color-nonce').val() + }).done( function( response ) { + if ( response.success ) { + $( 'body' ).removeClass( response.data.previousScheme ).addClass( response.data.currentScheme ); + } + }); + } + }); + + bindPasswordForm(); + bindPasswordResetLink(); + }); + + $( '#destroy-sessions' ).on( 'click', function( e ) { + var $this = $(this); + + wp.ajax.post( 'destroy-sessions', { + nonce: $( '#_wpnonce' ).val(), + user_id: $( '#user_id' ).val() + }).done( function( response ) { + $this.prop( 'disabled', true ); + $this.siblings( '.notice' ).remove(); + $this.before( '<div class="notice notice-success inline"><p>' + response.message + '</p></div>' ); + }).fail( function( response ) { + $this.siblings( '.notice' ).remove(); + $this.before( '<div class="notice notice-error inline"><p>' + response.message + '</p></div>' ); + }); + + 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.' ); + } + } ); + + /* + * 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); diff --git a/wp-admin/js/user-profile.min.js b/wp-admin/js/user-profile.min.js new file mode 100644 index 0000000..31f3609 --- /dev/null +++ b/wp-admin/js/user-profile.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(o){var e,a,t,n,i,r,p,d,l,c,u=!1,h=wp.i18n.__;function f(){"function"!=typeof zxcvbn?setTimeout(f,50):(!a.val()||c.hasClass("is-open")?(a.val(a.data("pw")),a.trigger("pwupdate")):b(),_(),m(),1!==parseInt(r.data("start-masked"),10)?a.attr("type","text"):r.trigger("click"),o("#pw-weak-text-label").text(h("Confirm use of weak password")),"mailserver_pass"!==a.prop("id")&&o(a).trigger("focus"))}function w(s){r.attr({"aria-label":h(s?"Show password":"Hide password")}).find(".text").text(h(s?"Show":"Hide")).end().find(".dashicons").removeClass(s?"dashicons-hidden":"dashicons-visibility").addClass(s?"dashicons-visibility":"dashicons-hidden")}function m(){r||(r=e.find(".wp-hide-pw")).show().on("click",function(){"password"===a.attr("type")?(a.attr("type","text"),w(!1)):(a.attr("type","password"),w(!0))})}function v(s,e,a){var t=o("<div />");t.addClass("notice inline"),t.addClass("notice-"+(e?"success":"error")),t.text(o(o.parseHTML(a)).text()).wrapInner("<p />"),s.prop("disabled",e),s.siblings(".notice").remove(),s.before(t)}function g(){var s;e=o(".user-pass1-wrap, .user-pass-wrap, .mailserver-pass-wrap, .reset-pass-submit"),o(".user-pass2-wrap").hide(),d=o("#submit, #wp-submit").on("click",function(){u=!1}),p=d.add(" #createusersub"),n=o(".pw-weak"),(i=n.find(".pw-checkbox")).on("change",function(){p.prop("disabled",!i.prop("checked"))}),(a=o("#pass1, #mailserver_pass")).length?(l=a.val(),1===parseInt(a.data("reveal"),10)&&f(),a.on("input pwupdate",function(){a.val()!==l&&(l=a.val(),a.removeClass("short bad good strong"),_())})):a=o("#user_pass"),t=o("#pass2").on("input",function(){0<t.val().length&&(a.val(t.val()),t.val(""),l="",a.trigger("pwupdate"))}),a.is(":hidden")&&(a.prop("disabled",!0),t.prop("disabled",!0)),c=e.find(".wp-pwd"),s=e.find("button.wp-generate-pw"),m(),s.show(),s.on("click",function(){u=!0,s.not(".skip-aria-expanded").attr("aria-expanded","true"),c.show().addClass("is-open"),a.attr("disabled",!1),t.attr("disabled",!1),f(),w(!1),wp.ajax.post("generate-password").done(function(s){a.data("pw",s)})}),e.find("button.wp-cancel-pw").on("click",function(){u=!1,a.prop("disabled",!0),t.prop("disabled",!0),a.val("").trigger("pwupdate"),w(!1),c.hide().removeClass("is-open"),p.prop("disabled",!1),s.attr("aria-expanded","false")}),e.closest("form").on("submit",function(){u=!1,a.prop("disabled",!1),t.prop("disabled",!1),t.val(a.val())})}function b(){var s=o("#pass1").val();if(o("#pass-strength-result").removeClass("short bad good strong empty"),s&&""!==s.trim())switch(wp.passwordStrength.meter(s,wp.passwordStrength.userInputDisallowedList(),s)){case-1:o("#pass-strength-result").addClass("bad").html(pwsL10n.unknown);break;case 2:o("#pass-strength-result").addClass("bad").html(pwsL10n.bad);break;case 3:o("#pass-strength-result").addClass("good").html(pwsL10n.good);break;case 4:o("#pass-strength-result").addClass("strong").html(pwsL10n.strong);break;case 5:o("#pass-strength-result").addClass("short").html(pwsL10n.mismatch);break;default:o("#pass-strength-result").addClass("short").html(pwsL10n.short)}else o("#pass-strength-result").addClass("empty").html(" ")}function _(){var s=o("#pass-strength-result");s.length&&(s=s[0]).className&&(a.addClass(s.className),o(s).is(".short, .bad")?(i.prop("checked")||p.prop("disabled",!0),n.show()):(o(s).is(".empty")?(p.prop("disabled",!0),i.prop("checked",!1)):p.prop("disabled",!1),n.hide()))}o(function(){var s,a,t,n,i=o("#display_name"),e=i.val(),r=o("#wp-admin-bar-my-account").find(".display-name");o("#pass1").val("").on("input pwupdate",b),o("#pass-strength-result").show(),o(".color-palette").on("click",function(){o(this).siblings('input[name="admin_color"]').prop("checked",!0)}),i.length&&(o("#first_name, #last_name, #nickname").on("blur.user_profile",function(){var a=[],t={display_nickname:o("#nickname").val()||"",display_username:o("#user_login").val()||"",display_firstname:o("#first_name").val()||"",display_lastname:o("#last_name").val()||""};t.display_firstname&&t.display_lastname&&(t.display_firstlast=t.display_firstname+" "+t.display_lastname,t.display_lastfirst=t.display_lastname+" "+t.display_firstname),o.each(o("option",i),function(s,e){a.push(e.value)}),o.each(t,function(s,e){e&&(e=e.replace(/<\/?[a-z][^>]*>/gi,""),t[s].length)&&-1===o.inArray(e,a)&&(a.push(e),o("<option />",{text:e}).appendTo(i))})}),i.on("change",function(){var s;t===n&&(s=this.value.trim()||e,r.text(s))})),s=o("#color-picker"),a=o("#colors-css"),t=o("input#user_id").val(),n=o('input[name="checkuser_id"]').val(),s.on("click.colorpicker",".color-option",function(){var s,e=o(this);if(!e.hasClass("selected")&&(e.siblings(".selected").removeClass("selected"),e.addClass("selected").find('input[type="radio"]').prop("checked",!0),t===n)){if((a=0===a.length?o('<link rel="stylesheet" />').appendTo("head"):a).attr("href",e.children(".css_url").val()),"undefined"!=typeof wp&&wp.svgPainter){try{s=JSON.parse(e.children(".icon_colors").val())}catch(s){}s&&(wp.svgPainter.setColors(s),wp.svgPainter.paint())}o.post(ajaxurl,{action:"save-user-color-scheme",color_scheme:e.children('input[name="admin_color"]').val(),nonce:o("#color-nonce").val()}).done(function(s){s.success&&o("body").removeClass(s.data.previousScheme).addClass(s.data.currentScheme)})}}),g(),o("#generate-reset-link").on("click",function(){var e=o(this),s={user_id:userProfileL10n.user_id,nonce:userProfileL10n.nonce},s=(e.parent().find(".notice-error").remove(),wp.ajax.post("send-password-reset",s));s.done(function(s){v(e,!0,s)}),s.fail(function(s){v(e,!1,s)})})}),o("#destroy-sessions").on("click",function(s){var e=o(this);wp.ajax.post("destroy-sessions",{nonce:o("#_wpnonce").val(),user_id:o("#user_id").val()}).done(function(s){e.prop("disabled",!0),e.siblings(".notice").remove(),e.before('<div class="notice notice-success inline"><p>'+s.message+"</p></div>")}).fail(function(s){e.siblings(".notice").remove(),e.before('<div class="notice notice-error inline"><p>'+s.message+"</p></div>")}),s.preventDefault()}),window.generatePassword=f,o(window).on("beforeunload",function(){if(!0===u)return h("Your new password has not been saved.")}),o(function(){o(".reset-pass-submit").length&&o(".reset-pass-submit button.wp-generate-pw").trigger("click")})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/user-suggest.js b/wp-admin/js/user-suggest.js new file mode 100644 index 0000000..f05b7ff --- /dev/null +++ b/wp-admin/js/user-suggest.js @@ -0,0 +1,64 @@ +/** + * Suggests users in a multisite environment. + * + * For input fields where the admin can select a user based on email or + * username, this script shows an autocompletion menu for these inputs. Should + * only be used in a multisite environment. Only users in the currently active + * site are shown. + * + * @since 3.4.0 + * @output wp-admin/js/user-suggest.js + */ + +/* global ajaxurl, current_site_id, isRtl */ + +(function( $ ) { + var id = ( typeof current_site_id !== 'undefined' ) ? '&site_id=' + current_site_id : ''; + $( function() { + var position = { offset: '0, -1' }; + if ( typeof isRtl !== 'undefined' && isRtl ) { + position.my = 'right top'; + position.at = 'right bottom'; + } + + /** + * Adds an autocomplete function to input fields marked with the class + * 'wp-suggest-user'. + * + * A minimum of two characters is required to trigger the suggestions. The + * autocompletion menu is shown at the left bottom of the input field. On + * RTL installations, it is shown at the right top. Adds the class 'open' to + * the input field when the autocompletion menu is shown. + * + * Does a backend call to retrieve the users. + * + * Optional data-attributes: + * - data-autocomplete-type (add, search) + * The action that is going to be performed: search for existing users + * or add a new one. Default: add + * - data-autocomplete-field (user_login, user_email) + * The field that is returned as the value for the suggestion. + * Default: user_login + * + * @see wp-admin/includes/admin-actions.php:wp_ajax_autocomplete_user() + */ + $( '.wp-suggest-user' ).each( function(){ + var $this = $( this ), + autocompleteType = ( typeof $this.data( 'autocompleteType' ) !== 'undefined' ) ? $this.data( 'autocompleteType' ) : 'add', + autocompleteField = ( typeof $this.data( 'autocompleteField' ) !== 'undefined' ) ? $this.data( 'autocompleteField' ) : 'user_login'; + + $this.autocomplete({ + source: ajaxurl + '?action=autocomplete-user&autocomplete_type=' + autocompleteType + '&autocomplete_field=' + autocompleteField + id, + delay: 500, + minLength: 2, + position: position, + open: function() { + $( this ).addClass( 'open' ); + }, + close: function() { + $( this ).removeClass( 'open' ); + } + }); + }); + }); +})( jQuery ); diff --git a/wp-admin/js/user-suggest.min.js b/wp-admin/js/user-suggest.min.js new file mode 100644 index 0000000..68c7bbb --- /dev/null +++ b/wp-admin/js/user-suggest.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(a){var n="undefined"!=typeof current_site_id?"&site_id="+current_site_id:"";a(function(){var i={offset:"0, -1"};"undefined"!=typeof isRtl&&isRtl&&(i.my="right top",i.at="right bottom"),a(".wp-suggest-user").each(function(){var e=a(this),t=void 0!==e.data("autocompleteType")?e.data("autocompleteType"):"add",o=void 0!==e.data("autocompleteField")?e.data("autocompleteField"):"user_login";e.autocomplete({source:ajaxurl+"?action=autocomplete-user&autocomplete_type="+t+"&autocomplete_field="+o+n,delay:500,minLength:2,position:i,open:function(){a(this).addClass("open")},close:function(){a(this).removeClass("open")}})})})}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/widgets.js b/wp-admin/js/widgets.js new file mode 100644 index 0000000..e8fc425 --- /dev/null +++ b/wp-admin/js/widgets.js @@ -0,0 +1,763 @@ +/** + * @output wp-admin/js/widgets.js + */ + +/* global ajaxurl, isRtl, wpWidgets */ + +(function($) { + var $document = $( document ); + +window.wpWidgets = { + /** + * A closed Sidebar that gets a Widget dragged over it. + * + * @var {element|null} + */ + hoveredSidebar: null, + + /** + * Lookup of which widgets have had change events triggered. + * + * @var {object} + */ + dirtyWidgets: {}, + + init : function() { + var rem, the_id, + self = this, + chooser = $('.widgets-chooser'), + selectSidebar = chooser.find('.widgets-chooser-sidebars'), + sidebars = $('div.widgets-sortables'), + isRTL = !! ( 'undefined' !== typeof isRtl && isRtl ); + + // Handle the widgets containers in the right column. + $( '#widgets-right .sidebar-name' ) + /* + * Toggle the widgets containers when clicked and update the toggle + * button `aria-expanded` attribute value. + */ + .on( 'click', function() { + var $this = $( this ), + $wrap = $this.closest( '.widgets-holder-wrap '), + $toggle = $this.find( '.handlediv' ); + + if ( $wrap.hasClass( 'closed' ) ) { + $wrap.removeClass( 'closed' ); + $toggle.attr( 'aria-expanded', 'true' ); + // Refresh the jQuery UI sortable items. + $this.parent().sortable( 'refresh' ); + } else { + $wrap.addClass( 'closed' ); + $toggle.attr( 'aria-expanded', 'false' ); + } + + // Update the admin menu "sticky" state. + $document.triggerHandler( 'wp-pin-menu' ); + }) + /* + * Set the initial `aria-expanded` attribute value on the widgets + * containers toggle button. The first one is expanded by default. + */ + .find( '.handlediv' ).each( function( index ) { + if ( 0 === index ) { + // jQuery equivalent of `continue` within an `each()` loop. + return; + } + + $( this ).attr( 'aria-expanded', 'false' ); + }); + + // Show AYS dialog when there are unsaved widget changes. + $( window ).on( 'beforeunload.widgets', function( event ) { + var dirtyWidgetIds = [], unsavedWidgetsElements; + $.each( self.dirtyWidgets, function( widgetId, dirty ) { + if ( dirty ) { + dirtyWidgetIds.push( widgetId ); + } + }); + if ( 0 !== dirtyWidgetIds.length ) { + unsavedWidgetsElements = $( '#widgets-right' ).find( '.widget' ).filter( function() { + return -1 !== dirtyWidgetIds.indexOf( $( this ).prop( 'id' ).replace( /^widget-\d+_/, '' ) ); + }); + unsavedWidgetsElements.each( function() { + if ( ! $( this ).hasClass( 'open' ) ) { + $( this ).find( '.widget-title-action:first' ).trigger( 'click' ); + } + }); + + // Bring the first unsaved widget into view and focus on the first tabbable field. + unsavedWidgetsElements.first().each( function() { + if ( this.scrollIntoViewIfNeeded ) { + this.scrollIntoViewIfNeeded(); + } else { + this.scrollIntoView(); + } + $( this ).find( '.widget-inside :tabbable:first' ).trigger( 'focus' ); + } ); + + event.returnValue = wp.i18n.__( 'The changes you made will be lost if you navigate away from this page.' ); + return event.returnValue; + } + }); + + // Handle the widgets containers in the left column. + $( '#widgets-left .sidebar-name' ).on( 'click', function() { + var $wrap = $( this ).closest( '.widgets-holder-wrap' ); + + $wrap + .toggleClass( 'closed' ) + .find( '.handlediv' ).attr( 'aria-expanded', ! $wrap.hasClass( 'closed' ) ); + + // Update the admin menu "sticky" state. + $document.triggerHandler( 'wp-pin-menu' ); + }); + + $(document.body).on('click.widgets-toggle', function(e) { + var target = $(e.target), css = {}, + widget, inside, targetWidth, widgetWidth, margin, saveButton, widgetId, + toggleBtn = target.closest( '.widget' ).find( '.widget-top button.widget-action' ); + + if ( target.parents('.widget-top').length && ! target.parents('#available-widgets').length ) { + widget = target.closest('div.widget'); + inside = widget.children('.widget-inside'); + targetWidth = parseInt( widget.find('input.widget-width').val(), 10 ); + widgetWidth = widget.parent().width(); + widgetId = inside.find( '.widget-id' ).val(); + + // Save button is initially disabled, but is enabled when a field is changed. + if ( ! widget.data( 'dirty-state-initialized' ) ) { + saveButton = inside.find( '.widget-control-save' ); + saveButton.prop( 'disabled', true ).val( wp.i18n.__( 'Saved' ) ); + inside.on( 'input change', function() { + self.dirtyWidgets[ widgetId ] = true; + widget.addClass( 'widget-dirty' ); + saveButton.prop( 'disabled', false ).val( wp.i18n.__( 'Save' ) ); + }); + widget.data( 'dirty-state-initialized', true ); + } + + if ( inside.is(':hidden') ) { + if ( targetWidth > 250 && ( targetWidth + 30 > widgetWidth ) && widget.closest('div.widgets-sortables').length ) { + if ( widget.closest('div.widget-liquid-right').length ) { + margin = isRTL ? 'margin-right' : 'margin-left'; + } else { + margin = isRTL ? 'margin-left' : 'margin-right'; + } + + css[ margin ] = widgetWidth - ( targetWidth + 30 ) + 'px'; + widget.css( css ); + } + /* + * Don't change the order of attributes changes and animation: + * it's important for screen readers, see ticket #31476. + */ + toggleBtn.attr( 'aria-expanded', 'true' ); + inside.slideDown( 'fast', function() { + widget.addClass( 'open' ); + }); + } else { + /* + * Don't change the order of attributes changes and animation: + * it's important for screen readers, see ticket #31476. + */ + toggleBtn.attr( 'aria-expanded', 'false' ); + inside.slideUp( 'fast', function() { + widget.attr( 'style', '' ); + widget.removeClass( 'open' ); + }); + } + } else if ( target.hasClass('widget-control-save') ) { + wpWidgets.save( target.closest('div.widget'), 0, 1, 0 ); + e.preventDefault(); + } else if ( target.hasClass('widget-control-remove') ) { + wpWidgets.save( target.closest('div.widget'), 1, 1, 0 ); + } else if ( target.hasClass('widget-control-close') ) { + widget = target.closest('div.widget'); + widget.removeClass( 'open' ); + toggleBtn.attr( 'aria-expanded', 'false' ); + wpWidgets.close( widget ); + } else if ( target.attr( 'id' ) === 'inactive-widgets-control-remove' ) { + wpWidgets.removeInactiveWidgets(); + e.preventDefault(); + } + }); + + sidebars.children('.widget').each( function() { + var $this = $(this); + + wpWidgets.appendTitle( this ); + + if ( $this.find( 'p.widget-error' ).length ) { + $this.find( '.widget-action' ).trigger( 'click' ).attr( 'aria-expanded', 'true' ); + } + }); + + $('#widget-list').children('.widget').draggable({ + connectToSortable: 'div.widgets-sortables', + handle: '> .widget-top > .widget-title', + distance: 2, + helper: 'clone', + zIndex: 101, + containment: '#wpwrap', + refreshPositions: true, + start: function( event, ui ) { + var chooser = $(this).find('.widgets-chooser'); + + ui.helper.find('div.widget-description').hide(); + the_id = this.id; + + if ( chooser.length ) { + // Hide the chooser and move it out of the widget. + $( '#wpbody-content' ).append( chooser.hide() ); + // Delete the cloned chooser from the drag helper. + ui.helper.find('.widgets-chooser').remove(); + self.clearWidgetSelection(); + } + }, + stop: function() { + if ( rem ) { + $(rem).hide(); + } + + rem = ''; + } + }); + + /** + * Opens and closes previously closed Sidebars when Widgets are dragged over/out of them. + */ + sidebars.droppable( { + tolerance: 'intersect', + + /** + * Open Sidebar when a Widget gets dragged over it. + * + * @ignore + * + * @param {Object} event jQuery event object. + */ + over: function( event ) { + var $wrap = $( event.target ).parent(); + + if ( wpWidgets.hoveredSidebar && ! $wrap.is( wpWidgets.hoveredSidebar ) ) { + // Close the previous Sidebar as the Widget has been dragged onto another Sidebar. + wpWidgets.closeSidebar( event ); + } + + if ( $wrap.hasClass( 'closed' ) ) { + wpWidgets.hoveredSidebar = $wrap; + $wrap + .removeClass( 'closed' ) + .find( '.handlediv' ).attr( 'aria-expanded', 'true' ); + } + + $( this ).sortable( 'refresh' ); + }, + + /** + * Close Sidebar when the Widget gets dragged out of it. + * + * @ignore + * + * @param {Object} event jQuery event object. + */ + out: function( event ) { + if ( wpWidgets.hoveredSidebar ) { + wpWidgets.closeSidebar( event ); + } + } + } ); + + sidebars.sortable({ + placeholder: 'widget-placeholder', + items: '> .widget', + handle: '> .widget-top > .widget-title', + cursor: 'move', + distance: 2, + containment: '#wpwrap', + tolerance: 'pointer', + refreshPositions: true, + start: function( event, ui ) { + var height, $this = $(this), + $wrap = $this.parent(), + inside = ui.item.children('.widget-inside'); + + if ( inside.css('display') === 'block' ) { + ui.item.removeClass('open'); + ui.item.find( '.widget-top button.widget-action' ).attr( 'aria-expanded', 'false' ); + inside.hide(); + $(this).sortable('refreshPositions'); + } + + if ( ! $wrap.hasClass('closed') ) { + // Lock all open sidebars min-height when starting to drag. + // Prevents jumping when dragging a widget from an open sidebar to a closed sidebar below. + height = ui.item.hasClass('ui-draggable') ? $this.height() : 1 + $this.height(); + $this.css( 'min-height', height + 'px' ); + } + }, + + stop: function( event, ui ) { + var addNew, widgetNumber, $sidebar, $children, child, item, + $widget = ui.item, + id = the_id; + + // Reset the var to hold a previously closed sidebar. + wpWidgets.hoveredSidebar = null; + + if ( $widget.hasClass('deleting') ) { + wpWidgets.save( $widget, 1, 0, 1 ); // Delete widget. + $widget.remove(); + return; + } + + addNew = $widget.find('input.add_new').val(); + widgetNumber = $widget.find('input.multi_number').val(); + + $widget.attr( 'style', '' ).removeClass('ui-draggable'); + the_id = ''; + + if ( addNew ) { + if ( 'multi' === addNew ) { + $widget.html( + $widget.html().replace( /<[^<>]+>/g, function( tag ) { + return tag.replace( /__i__|%i%/g, widgetNumber ); + }) + ); + + $widget.attr( 'id', id.replace( '__i__', widgetNumber ) ); + widgetNumber++; + + $( 'div#' + id ).find( 'input.multi_number' ).val( widgetNumber ); + } else if ( 'single' === addNew ) { + $widget.attr( 'id', 'new-' + id ); + rem = 'div#' + id; + } + + wpWidgets.save( $widget, 0, 0, 1 ); + $widget.find('input.add_new').val(''); + $document.trigger( 'widget-added', [ $widget ] ); + } + + $sidebar = $widget.parent(); + + if ( $sidebar.parent().hasClass('closed') ) { + $sidebar.parent() + .removeClass( 'closed' ) + .find( '.handlediv' ).attr( 'aria-expanded', 'true' ); + + $children = $sidebar.children('.widget'); + + // Make sure the dropped widget is at the top. + if ( $children.length > 1 ) { + child = $children.get(0); + item = $widget.get(0); + + if ( child.id && item.id && child.id !== item.id ) { + $( child ).before( $widget ); + } + } + } + + if ( addNew ) { + $widget.find( '.widget-action' ).trigger( 'click' ); + } else { + wpWidgets.saveOrder( $sidebar.attr('id') ); + } + }, + + activate: function() { + $(this).parent().addClass( 'widget-hover' ); + }, + + deactivate: function() { + // Remove all min-height added on "start". + $(this).css( 'min-height', '' ).parent().removeClass( 'widget-hover' ); + }, + + receive: function( event, ui ) { + var $sender = $( ui.sender ); + + // Don't add more widgets to orphaned sidebars. + if ( this.id.indexOf('orphaned_widgets') > -1 ) { + $sender.sortable('cancel'); + return; + } + + // If the last widget was moved out of an orphaned sidebar, close and remove it. + if ( $sender.attr('id').indexOf('orphaned_widgets') > -1 && ! $sender.children('.widget').length ) { + $sender.parents('.orphan-sidebar').slideUp( 400, function(){ $(this).remove(); } ); + } + } + }).sortable( 'option', 'connectWith', 'div.widgets-sortables' ); + + $('#available-widgets').droppable({ + tolerance: 'pointer', + accept: function(o){ + return $(o).parent().attr('id') !== 'widget-list'; + }, + drop: function(e,ui) { + ui.draggable.addClass('deleting'); + $('#removing-widget').hide().children('span').empty(); + }, + over: function(e,ui) { + ui.draggable.addClass('deleting'); + $('div.widget-placeholder').hide(); + + if ( ui.draggable.hasClass('ui-sortable-helper') ) { + $('#removing-widget').show().children('span') + .html( ui.draggable.find( 'div.widget-title' ).children( 'h3' ).html() ); + } + }, + out: function(e,ui) { + ui.draggable.removeClass('deleting'); + $('div.widget-placeholder').show(); + $('#removing-widget').hide().children('span').empty(); + } + }); + + // Area Chooser. + $( '#widgets-right .widgets-holder-wrap' ).each( function( index, element ) { + var $element = $( element ), + name = $element.find( '.sidebar-name h2' ).text() || '', + ariaLabel = $element.find( '.sidebar-name' ).data( 'add-to' ), + id = $element.find( '.widgets-sortables' ).attr( 'id' ), + li = $( '<li>' ), + button = $( '<button>', { + type: 'button', + 'aria-pressed': 'false', + 'class': 'widgets-chooser-button', + 'aria-label': ariaLabel + } ).text( name.toString().trim() ); + + li.append( button ); + + if ( index === 0 ) { + li.addClass( 'widgets-chooser-selected' ); + button.attr( 'aria-pressed', 'true' ); + } + + selectSidebar.append( li ); + li.data( 'sidebarId', id ); + }); + + $( '#available-widgets .widget .widget-top' ).on( 'click.widgets-chooser', function() { + var $widget = $( this ).closest( '.widget' ), + toggleButton = $( this ).find( '.widget-action' ), + chooserButtons = selectSidebar.find( '.widgets-chooser-button' ); + + if ( $widget.hasClass( 'widget-in-question' ) || $( '#widgets-left' ).hasClass( 'chooser' ) ) { + toggleButton.attr( 'aria-expanded', 'false' ); + self.closeChooser(); + } else { + // Open the chooser. + self.clearWidgetSelection(); + $( '#widgets-left' ).addClass( 'chooser' ); + // Add CSS class and insert the chooser after the widget description. + $widget.addClass( 'widget-in-question' ).children( '.widget-description' ).after( chooser ); + // Open the chooser with a slide down animation. + chooser.slideDown( 300, function() { + // Update the toggle button aria-expanded attribute after previous DOM manipulations. + toggleButton.attr( 'aria-expanded', 'true' ); + }); + + chooserButtons.on( 'click.widgets-chooser', function() { + selectSidebar.find( '.widgets-chooser-selected' ).removeClass( 'widgets-chooser-selected' ); + chooserButtons.attr( 'aria-pressed', 'false' ); + $( this ) + .attr( 'aria-pressed', 'true' ) + .closest( 'li' ).addClass( 'widgets-chooser-selected' ); + } ); + } + }); + + // Add event handlers. + chooser.on( 'click.widgets-chooser', function( event ) { + var $target = $( event.target ); + + if ( $target.hasClass('button-primary') ) { + self.addWidget( chooser ); + self.closeChooser(); + } else if ( $target.hasClass( 'widgets-chooser-cancel' ) ) { + self.closeChooser(); + } + }).on( 'keyup.widgets-chooser', function( event ) { + if ( event.which === $.ui.keyCode.ESCAPE ) { + self.closeChooser(); + } + }); + }, + + saveOrder : function( sidebarId ) { + var data = { + action: 'widgets-order', + savewidgets: $('#_wpnonce_widgets').val(), + sidebars: [] + }; + + if ( sidebarId ) { + $( '#' + sidebarId ).find( '.spinner:first' ).addClass( 'is-active' ); + } + + $('div.widgets-sortables').each( function() { + if ( $(this).sortable ) { + data['sidebars[' + $(this).attr('id') + ']'] = $(this).sortable('toArray').join(','); + } + }); + + $.post( ajaxurl, data, function() { + $( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length ); + $( '.spinner' ).removeClass( 'is-active' ); + }); + }, + + save : function( widget, del, animate, order ) { + var self = this, data, a, + sidebarId = widget.closest( 'div.widgets-sortables' ).attr( 'id' ), + form = widget.find( 'form' ), + isAdd = widget.find( 'input.add_new' ).val(); + + if ( ! del && ! isAdd && form.prop( 'checkValidity' ) && ! form[0].checkValidity() ) { + return; + } + + data = form.serialize(); + + widget = $(widget); + $( '.spinner', widget ).addClass( 'is-active' ); + + a = { + action: 'save-widget', + savewidgets: $('#_wpnonce_widgets').val(), + sidebar: sidebarId + }; + + if ( del ) { + a.delete_widget = 1; + } + + data += '&' + $.param(a); + + $.post( ajaxurl, data, function(r) { + var id = $('input.widget-id', widget).val(); + + if ( del ) { + if ( ! $('input.widget_number', widget).val() ) { + $('#available-widgets').find('input.widget-id').each(function(){ + if ( $(this).val() === id ) { + $(this).closest('div.widget').show(); + } + }); + } + + if ( animate ) { + order = 0; + widget.slideUp( 'fast', function() { + $( this ).remove(); + wpWidgets.saveOrder(); + delete self.dirtyWidgets[ id ]; + }); + } else { + widget.remove(); + delete self.dirtyWidgets[ id ]; + + if ( sidebarId === 'wp_inactive_widgets' ) { + $( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length ); + } + } + } else { + $( '.spinner' ).removeClass( 'is-active' ); + if ( r && r.length > 2 ) { + $( 'div.widget-content', widget ).html( r ); + wpWidgets.appendTitle( widget ); + + // Re-disable the save button. + widget.find( '.widget-control-save' ).prop( 'disabled', true ).val( wp.i18n.__( 'Saved' ) ); + + widget.removeClass( 'widget-dirty' ); + + // Clear the dirty flag from the widget. + delete self.dirtyWidgets[ id ]; + + $document.trigger( 'widget-updated', [ widget ] ); + + if ( sidebarId === 'wp_inactive_widgets' ) { + $( '#inactive-widgets-control-remove' ).prop( 'disabled' , ! $( '#wp_inactive_widgets .widget' ).length ); + } + } + } + + if ( order ) { + wpWidgets.saveOrder(); + } + }); + }, + + removeInactiveWidgets : function() { + var $element = $( '.remove-inactive-widgets' ), self = this, a, data; + + $( '.spinner', $element ).addClass( 'is-active' ); + + a = { + action : 'delete-inactive-widgets', + removeinactivewidgets : $( '#_wpnonce_remove_inactive_widgets' ).val() + }; + + data = $.param( a ); + + $.post( ajaxurl, data, function() { + $( '#wp_inactive_widgets .widget' ).each(function() { + var $widget = $( this ); + delete self.dirtyWidgets[ $widget.find( 'input.widget-id' ).val() ]; + $widget.remove(); + }); + $( '#inactive-widgets-control-remove' ).prop( 'disabled', true ); + $( '.spinner', $element ).removeClass( 'is-active' ); + } ); + }, + + appendTitle : function(widget) { + var title = $('input[id*="-title"]', widget).val() || ''; + + if ( title ) { + title = ': ' + title.replace(/<[^<>]+>/g, '').replace(/</g, '<').replace(/>/g, '>'); + } + + $(widget).children('.widget-top').children('.widget-title').children() + .children('.in-widget-title').html(title); + + }, + + close : function(widget) { + widget.children('.widget-inside').slideUp('fast', function() { + widget.attr( 'style', '' ) + .find( '.widget-top button.widget-action' ) + .attr( 'aria-expanded', 'false' ) + .focus(); + }); + }, + + addWidget: function( chooser ) { + var widget, widgetId, add, n, viewportTop, viewportBottom, sidebarBounds, + sidebarId = chooser.find( '.widgets-chooser-selected' ).data('sidebarId'), + sidebar = $( '#' + sidebarId ); + + widget = $('#available-widgets').find('.widget-in-question').clone(); + widgetId = widget.attr('id'); + add = widget.find( 'input.add_new' ).val(); + n = widget.find( 'input.multi_number' ).val(); + + // Remove the cloned chooser from the widget. + widget.find('.widgets-chooser').remove(); + + if ( 'multi' === add ) { + widget.html( + widget.html().replace( /<[^<>]+>/g, function(m) { + return m.replace( /__i__|%i%/g, n ); + }) + ); + + widget.attr( 'id', widgetId.replace( '__i__', n ) ); + n++; + $( '#' + widgetId ).find('input.multi_number').val(n); + } else if ( 'single' === add ) { + widget.attr( 'id', 'new-' + widgetId ); + $( '#' + widgetId ).hide(); + } + + // Open the widgets container. + sidebar.closest( '.widgets-holder-wrap' ) + .removeClass( 'closed' ) + .find( '.handlediv' ).attr( 'aria-expanded', 'true' ); + + sidebar.append( widget ); + sidebar.sortable('refresh'); + + wpWidgets.save( widget, 0, 0, 1 ); + // No longer "new" widget. + widget.find( 'input.add_new' ).val(''); + + $document.trigger( 'widget-added', [ widget ] ); + + /* + * Check if any part of the sidebar is visible in the viewport. If it is, don't scroll. + * Otherwise, scroll up to so the sidebar is in view. + * + * We do this by comparing the top and bottom, of the sidebar so see if they are within + * the bounds of the viewport. + */ + viewportTop = $(window).scrollTop(); + viewportBottom = viewportTop + $(window).height(); + sidebarBounds = sidebar.offset(); + + sidebarBounds.bottom = sidebarBounds.top + sidebar.outerHeight(); + + if ( viewportTop > sidebarBounds.bottom || viewportBottom < sidebarBounds.top ) { + $( 'html, body' ).animate({ + scrollTop: sidebarBounds.top - 130 + }, 200 ); + } + + window.setTimeout( function() { + // Cannot use a callback in the animation above as it fires twice, + // have to queue this "by hand". + widget.find( '.widget-title' ).trigger('click'); + // At the end of the animation, announce the widget has been added. + window.wp.a11y.speak( wp.i18n.__( 'Widget has been added to the selected sidebar' ), 'assertive' ); + }, 250 ); + }, + + closeChooser: function() { + var self = this, + widgetInQuestion = $( '#available-widgets .widget-in-question' ); + + $( '.widgets-chooser' ).slideUp( 200, function() { + $( '#wpbody-content' ).append( this ); + self.clearWidgetSelection(); + // Move focus back to the toggle button. + widgetInQuestion.find( '.widget-action' ).attr( 'aria-expanded', 'false' ).focus(); + }); + }, + + clearWidgetSelection: function() { + $( '#widgets-left' ).removeClass( 'chooser' ); + $( '.widget-in-question' ).removeClass( 'widget-in-question' ); + }, + + /** + * Closes a Sidebar that was previously closed, but opened by dragging a Widget over it. + * + * Used when a Widget gets dragged in/out of the Sidebar and never dropped. + * + * @param {Object} event jQuery event object. + */ + closeSidebar: function( event ) { + this.hoveredSidebar + .addClass( 'closed' ) + .find( '.handlediv' ).attr( 'aria-expanded', 'false' ); + + $( event.target ).css( 'min-height', '' ); + this.hoveredSidebar = null; + } +}; + +$( function(){ wpWidgets.init(); } ); + +})(jQuery); + +/** + * Removed in 5.5.0, needed for back-compatibility. + * + * @since 4.9.0 + * @deprecated 5.5.0 + * + * @type {object} +*/ +wpWidgets.l10n = wpWidgets.l10n || { + save: '', + saved: '', + saveAlert: '', + widgetAdded: '' +}; + +wpWidgets.l10n = window.wp.deprecateL10nObject( 'wpWidgets.l10n', wpWidgets.l10n, '5.5.0' ); diff --git a/wp-admin/js/widgets.min.js b/wp-admin/js/widgets.min.js new file mode 100644 index 0000000..ed0a7b1 --- /dev/null +++ b/wp-admin/js/widgets.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(w){var l=w(document);window.wpWidgets={hoveredSidebar:null,dirtyWidgets:{},init:function(){var r,o,g=this,d=w(".widgets-chooser"),s=d.find(".widgets-chooser-sidebars"),e=w("div.widgets-sortables"),c=!("undefined"==typeof isRtl||!isRtl);w("#widgets-right .sidebar-name").on("click",function(){var e=w(this),i=e.closest(".widgets-holder-wrap "),t=e.find(".handlediv");i.hasClass("closed")?(i.removeClass("closed"),t.attr("aria-expanded","true"),e.parent().sortable("refresh")):(i.addClass("closed"),t.attr("aria-expanded","false")),l.triggerHandler("wp-pin-menu")}).find(".handlediv").each(function(e){0!==e&&w(this).attr("aria-expanded","false")}),w(window).on("beforeunload.widgets",function(e){var i,t=[];if(w.each(g.dirtyWidgets,function(e,i){i&&t.push(e)}),0!==t.length)return(i=w("#widgets-right").find(".widget").filter(function(){return-1!==t.indexOf(w(this).prop("id").replace(/^widget-\d+_/,""))})).each(function(){w(this).hasClass("open")||w(this).find(".widget-title-action:first").trigger("click")}),i.first().each(function(){this.scrollIntoViewIfNeeded?this.scrollIntoViewIfNeeded():this.scrollIntoView(),w(this).find(".widget-inside :tabbable:first").trigger("focus")}),e.returnValue=wp.i18n.__("The changes you made will be lost if you navigate away from this page."),e.returnValue}),w("#widgets-left .sidebar-name").on("click",function(){var e=w(this).closest(".widgets-holder-wrap");e.toggleClass("closed").find(".handlediv").attr("aria-expanded",!e.hasClass("closed")),l.triggerHandler("wp-pin-menu")}),w(document.body).on("click.widgets-toggle",function(e){var i,t,d,a,s,n,r=w(e.target),o={},l=r.closest(".widget").find(".widget-top button.widget-action");r.parents(".widget-top").length&&!r.parents("#available-widgets").length?(t=(i=r.closest("div.widget")).children(".widget-inside"),d=parseInt(i.find("input.widget-width").val(),10),a=i.parent().width(),n=t.find(".widget-id").val(),i.data("dirty-state-initialized")||((s=t.find(".widget-control-save")).prop("disabled",!0).val(wp.i18n.__("Saved")),t.on("input change",function(){g.dirtyWidgets[n]=!0,i.addClass("widget-dirty"),s.prop("disabled",!1).val(wp.i18n.__("Save"))}),i.data("dirty-state-initialized",!0)),t.is(":hidden")?(250<d&&a<d+30&&i.closest("div.widgets-sortables").length&&(o[i.closest("div.widget-liquid-right").length?c?"margin-right":"margin-left":c?"margin-left":"margin-right"]=a-(d+30)+"px",i.css(o)),l.attr("aria-expanded","true"),t.slideDown("fast",function(){i.addClass("open")})):(l.attr("aria-expanded","false"),t.slideUp("fast",function(){i.attr("style",""),i.removeClass("open")}))):r.hasClass("widget-control-save")?(wpWidgets.save(r.closest("div.widget"),0,1,0),e.preventDefault()):r.hasClass("widget-control-remove")?wpWidgets.save(r.closest("div.widget"),1,1,0):r.hasClass("widget-control-close")?((i=r.closest("div.widget")).removeClass("open"),l.attr("aria-expanded","false"),wpWidgets.close(i)):"inactive-widgets-control-remove"===r.attr("id")&&(wpWidgets.removeInactiveWidgets(),e.preventDefault())}),e.children(".widget").each(function(){var e=w(this);wpWidgets.appendTitle(this),e.find("p.widget-error").length&&e.find(".widget-action").trigger("click").attr("aria-expanded","true")}),w("#widget-list").children(".widget").draggable({connectToSortable:"div.widgets-sortables",handle:"> .widget-top > .widget-title",distance:2,helper:"clone",zIndex:101,containment:"#wpwrap",refreshPositions:!0,start:function(e,i){var t=w(this).find(".widgets-chooser");i.helper.find("div.widget-description").hide(),o=this.id,t.length&&(w("#wpbody-content").append(t.hide()),i.helper.find(".widgets-chooser").remove(),g.clearWidgetSelection())},stop:function(){r&&w(r).hide(),r=""}}),e.droppable({tolerance:"intersect",over:function(e){var i=w(e.target).parent();wpWidgets.hoveredSidebar&&!i.is(wpWidgets.hoveredSidebar)&&wpWidgets.closeSidebar(e),i.hasClass("closed")&&(wpWidgets.hoveredSidebar=i).removeClass("closed").find(".handlediv").attr("aria-expanded","true"),w(this).sortable("refresh")},out:function(e){wpWidgets.hoveredSidebar&&wpWidgets.closeSidebar(e)}}),e.sortable({placeholder:"widget-placeholder",items:"> .widget",handle:"> .widget-top > .widget-title",cursor:"move",distance:2,containment:"#wpwrap",tolerance:"pointer",refreshPositions:!0,start:function(e,i){var t=w(this),d=t.parent(),a=i.item.children(".widget-inside");"block"===a.css("display")&&(i.item.removeClass("open"),i.item.find(".widget-top button.widget-action").attr("aria-expanded","false"),a.hide(),w(this).sortable("refreshPositions")),d.hasClass("closed")||(a=i.item.hasClass("ui-draggable")?t.height():1+t.height(),t.css("min-height",a+"px"))},stop:function(e,i){var t,d,a,s,i=i.item,n=o;wpWidgets.hoveredSidebar=null,i.hasClass("deleting")?(wpWidgets.save(i,1,0,1),i.remove()):(t=i.find("input.add_new").val(),d=i.find("input.multi_number").val(),i.attr("style","").removeClass("ui-draggable"),o="",t&&("multi"===t?(i.html(i.html().replace(/<[^<>]+>/g,function(e){return e.replace(/__i__|%i%/g,d)})),i.attr("id",n.replace("__i__",d)),d++,w("div#"+n).find("input.multi_number").val(d)):"single"===t&&(i.attr("id","new-"+n),r="div#"+n),wpWidgets.save(i,0,0,1),i.find("input.add_new").val(""),l.trigger("widget-added",[i])),(n=i.parent()).parent().hasClass("closed")&&(n.parent().removeClass("closed").find(".handlediv").attr("aria-expanded","true"),1<(a=n.children(".widget")).length)&&(a=a.get(0),s=i.get(0),a.id)&&s.id&&a.id!==s.id&&w(a).before(i),t?i.find(".widget-action").trigger("click"):wpWidgets.saveOrder(n.attr("id")))},activate:function(){w(this).parent().addClass("widget-hover")},deactivate:function(){w(this).css("min-height","").parent().removeClass("widget-hover")},receive:function(e,i){i=w(i.sender);-1<this.id.indexOf("orphaned_widgets")?i.sortable("cancel"):-1<i.attr("id").indexOf("orphaned_widgets")&&!i.children(".widget").length&&i.parents(".orphan-sidebar").slideUp(400,function(){w(this).remove()})}}).sortable("option","connectWith","div.widgets-sortables"),w("#available-widgets").droppable({tolerance:"pointer",accept:function(e){return"widget-list"!==w(e).parent().attr("id")},drop:function(e,i){i.draggable.addClass("deleting"),w("#removing-widget").hide().children("span").empty()},over:function(e,i){i.draggable.addClass("deleting"),w("div.widget-placeholder").hide(),i.draggable.hasClass("ui-sortable-helper")&&w("#removing-widget").show().children("span").html(i.draggable.find("div.widget-title").children("h3").html())},out:function(e,i){i.draggable.removeClass("deleting"),w("div.widget-placeholder").show(),w("#removing-widget").hide().children("span").empty()}}),w("#widgets-right .widgets-holder-wrap").each(function(e,i){var i=w(i),t=i.find(".sidebar-name h2").text()||"",d=i.find(".sidebar-name").data("add-to"),i=i.find(".widgets-sortables").attr("id"),a=w("<li>"),d=w("<button>",{type:"button","aria-pressed":"false",class:"widgets-chooser-button","aria-label":d}).text(t.toString().trim());a.append(d),0===e&&(a.addClass("widgets-chooser-selected"),d.attr("aria-pressed","true")),s.append(a),a.data("sidebarId",i)}),w("#available-widgets .widget .widget-top").on("click.widgets-chooser",function(){var e=w(this).closest(".widget"),i=w(this).find(".widget-action"),t=s.find(".widgets-chooser-button");e.hasClass("widget-in-question")||w("#widgets-left").hasClass("chooser")?(i.attr("aria-expanded","false"),g.closeChooser()):(g.clearWidgetSelection(),w("#widgets-left").addClass("chooser"),e.addClass("widget-in-question").children(".widget-description").after(d),d.slideDown(300,function(){i.attr("aria-expanded","true")}),t.on("click.widgets-chooser",function(){s.find(".widgets-chooser-selected").removeClass("widgets-chooser-selected"),t.attr("aria-pressed","false"),w(this).attr("aria-pressed","true").closest("li").addClass("widgets-chooser-selected")}))}),d.on("click.widgets-chooser",function(e){e=w(e.target);e.hasClass("button-primary")?(g.addWidget(d),g.closeChooser()):e.hasClass("widgets-chooser-cancel")&&g.closeChooser()}).on("keyup.widgets-chooser",function(e){e.which===w.ui.keyCode.ESCAPE&&g.closeChooser()})},saveOrder:function(e){var i={action:"widgets-order",savewidgets:w("#_wpnonce_widgets").val(),sidebars:[]};e&&w("#"+e).find(".spinner:first").addClass("is-active"),w("div.widgets-sortables").each(function(){w(this).sortable&&(i["sidebars["+w(this).attr("id")+"]"]=w(this).sortable("toArray").join(","))}),w.post(ajaxurl,i,function(){w("#inactive-widgets-control-remove").prop("disabled",!w("#wp_inactive_widgets .widget").length),w(".spinner").removeClass("is-active")})},save:function(t,d,a,s){var n=this,r=t.closest("div.widgets-sortables").attr("id"),e=t.find("form"),i=t.find("input.add_new").val();(d||i||!e.prop("checkValidity")||e[0].checkValidity())&&(i=e.serialize(),t=w(t),w(".spinner",t).addClass("is-active"),e={action:"save-widget",savewidgets:w("#_wpnonce_widgets").val(),sidebar:r},d&&(e.delete_widget=1),i+="&"+w.param(e),w.post(ajaxurl,i,function(e){var i=w("input.widget-id",t).val();d?(w("input.widget_number",t).val()||w("#available-widgets").find("input.widget-id").each(function(){w(this).val()===i&&w(this).closest("div.widget").show()}),a?(s=0,t.slideUp("fast",function(){w(this).remove(),wpWidgets.saveOrder(),delete n.dirtyWidgets[i]})):(t.remove(),delete n.dirtyWidgets[i],"wp_inactive_widgets"===r&&w("#inactive-widgets-control-remove").prop("disabled",!w("#wp_inactive_widgets .widget").length))):(w(".spinner").removeClass("is-active"),e&&2<e.length&&(w("div.widget-content",t).html(e),wpWidgets.appendTitle(t),t.find(".widget-control-save").prop("disabled",!0).val(wp.i18n.__("Saved")),t.removeClass("widget-dirty"),delete n.dirtyWidgets[i],l.trigger("widget-updated",[t]),"wp_inactive_widgets"===r)&&w("#inactive-widgets-control-remove").prop("disabled",!w("#wp_inactive_widgets .widget").length)),s&&wpWidgets.saveOrder()}))},removeInactiveWidgets:function(){var e,i=w(".remove-inactive-widgets"),t=this;w(".spinner",i).addClass("is-active"),e={action:"delete-inactive-widgets",removeinactivewidgets:w("#_wpnonce_remove_inactive_widgets").val()},e=w.param(e),w.post(ajaxurl,e,function(){w("#wp_inactive_widgets .widget").each(function(){var e=w(this);delete t.dirtyWidgets[e.find("input.widget-id").val()],e.remove()}),w("#inactive-widgets-control-remove").prop("disabled",!0),w(".spinner",i).removeClass("is-active")})},appendTitle:function(e){var i=(i=w('input[id*="-title"]',e).val()||"")&&": "+i.replace(/<[^<>]+>/g,"").replace(/</g,"<").replace(/>/g,">");w(e).children(".widget-top").children(".widget-title").children().children(".in-widget-title").html(i)},close:function(e){e.children(".widget-inside").slideUp("fast",function(){e.attr("style","").find(".widget-top button.widget-action").attr("aria-expanded","false").focus()})},addWidget:function(e){var i,e=e.find(".widgets-chooser-selected").data("sidebarId"),e=w("#"+e),t=w("#available-widgets").find(".widget-in-question").clone(),d=t.attr("id"),a=t.find("input.add_new").val(),s=t.find("input.multi_number").val();t.find(".widgets-chooser").remove(),"multi"===a?(t.html(t.html().replace(/<[^<>]+>/g,function(e){return e.replace(/__i__|%i%/g,s)})),t.attr("id",d.replace("__i__",s)),s++,w("#"+d).find("input.multi_number").val(s)):"single"===a&&(t.attr("id","new-"+d),w("#"+d).hide()),e.closest(".widgets-holder-wrap").removeClass("closed").find(".handlediv").attr("aria-expanded","true"),e.append(t),e.sortable("refresh"),wpWidgets.save(t,0,0,1),t.find("input.add_new").val(""),l.trigger("widget-added",[t]),d=(a=w(window).scrollTop())+w(window).height(),(i=e.offset()).bottom=i.top+e.outerHeight(),(a>i.bottom||d<i.top)&&w("html, body").animate({scrollTop:i.top-130},200),window.setTimeout(function(){t.find(".widget-title").trigger("click"),window.wp.a11y.speak(wp.i18n.__("Widget has been added to the selected sidebar"),"assertive")},250)},closeChooser:function(){var e=this,i=w("#available-widgets .widget-in-question");w(".widgets-chooser").slideUp(200,function(){w("#wpbody-content").append(this),e.clearWidgetSelection(),i.find(".widget-action").attr("aria-expanded","false").focus()})},clearWidgetSelection:function(){w("#widgets-left").removeClass("chooser"),w(".widget-in-question").removeClass("widget-in-question")},closeSidebar:function(e){this.hoveredSidebar.addClass("closed").find(".handlediv").attr("aria-expanded","false"),w(e.target).css("min-height",""),this.hoveredSidebar=null}},w(function(){wpWidgets.init()})}(jQuery),wpWidgets.l10n=wpWidgets.l10n||{save:"",saved:"",saveAlert:"",widgetAdded:""},wpWidgets.l10n=window.wp.deprecateL10nObject("wpWidgets.l10n",wpWidgets.l10n,"5.5.0");
\ No newline at end of file diff --git a/wp-admin/js/widgets/custom-html-widgets.js b/wp-admin/js/widgets/custom-html-widgets.js new file mode 100644 index 0000000..e36d115 --- /dev/null +++ b/wp-admin/js/widgets/custom-html-widgets.js @@ -0,0 +1,462 @@ +/** + * @output wp-admin/js/widgets/custom-html-widgets.js + */ + +/* global wp */ +/* eslint consistent-this: [ "error", "control" ] */ +/* eslint no-magic-numbers: ["error", { "ignore": [0,1,-1] }] */ + +/** + * @namespace wp.customHtmlWidget + * @memberOf wp + */ +wp.customHtmlWidgets = ( function( $ ) { + 'use strict'; + + var component = { + idBases: [ 'custom_html' ], + codeEditorSettings: {}, + l10n: { + errorNotice: { + singular: '', + plural: '' + } + } + }; + + component.CustomHtmlWidgetControl = Backbone.View.extend(/** @lends wp.customHtmlWidgets.CustomHtmlWidgetControl.prototype */{ + + /** + * View events. + * + * @type {Object} + */ + events: {}, + + /** + * Text widget control. + * + * @constructs wp.customHtmlWidgets.CustomHtmlWidgetControl + * @augments Backbone.View + * @abstract + * + * @param {Object} options - Options. + * @param {jQuery} options.el - Control field container element. + * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. + * + * @return {void} + */ + initialize: function initialize( options ) { + var control = this; + + if ( ! options.el ) { + throw new Error( 'Missing options.el' ); + } + if ( ! options.syncContainer ) { + throw new Error( 'Missing options.syncContainer' ); + } + + Backbone.View.prototype.initialize.call( control, options ); + control.syncContainer = options.syncContainer; + control.widgetIdBase = control.syncContainer.parent().find( '.id_base' ).val(); + control.widgetNumber = control.syncContainer.parent().find( '.widget_number' ).val(); + control.customizeSettingId = 'widget_' + control.widgetIdBase + '[' + String( control.widgetNumber ) + ']'; + + control.$el.addClass( 'custom-html-widget-fields' ); + control.$el.html( wp.template( 'widget-custom-html-control-fields' )( { codeEditorDisabled: component.codeEditorSettings.disabled } ) ); + + control.errorNoticeContainer = control.$el.find( '.code-editor-error-container' ); + control.currentErrorAnnotations = []; + control.saveButton = control.syncContainer.add( control.syncContainer.parent().find( '.widget-control-actions' ) ).find( '.widget-control-save, #savewidget' ); + control.saveButton.addClass( 'custom-html-widget-save-button' ); // To facilitate style targeting. + + control.fields = { + title: control.$el.find( '.title' ), + content: control.$el.find( '.content' ) + }; + + // Sync input fields to hidden sync fields which actually get sent to the server. + _.each( control.fields, function( fieldInput, fieldName ) { + fieldInput.on( 'input change', function updateSyncField() { + var syncInput = control.syncContainer.find( '.sync-input.' + fieldName ); + if ( syncInput.val() !== fieldInput.val() ) { + syncInput.val( fieldInput.val() ); + syncInput.trigger( 'change' ); + } + }); + + // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event. + fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() ); + }); + }, + + /** + * Update input fields from the sync fields. + * + * This function is called at the widget-updated and widget-synced events. + * A field will only be updated if it is not currently focused, to avoid + * overwriting content that the user is entering. + * + * @return {void} + */ + updateFields: function updateFields() { + var control = this, syncInput; + + if ( ! control.fields.title.is( document.activeElement ) ) { + syncInput = control.syncContainer.find( '.sync-input.title' ); + control.fields.title.val( syncInput.val() ); + } + + /* + * Prevent updating content when the editor is focused or if there are current error annotations, + * to prevent the editor's contents from getting sanitized as soon as a user removes focus from + * the editor. This is particularly important for users who cannot unfiltered_html. + */ + control.contentUpdateBypassed = control.fields.content.is( document.activeElement ) || control.editor && control.editor.codemirror.state.focused || 0 !== control.currentErrorAnnotations.length; + if ( ! control.contentUpdateBypassed ) { + syncInput = control.syncContainer.find( '.sync-input.content' ); + control.fields.content.val( syncInput.val() ); + } + }, + + /** + * Show linting error notice. + * + * @param {Array} errorAnnotations - Error annotations. + * @return {void} + */ + updateErrorNotice: function( errorAnnotations ) { + var control = this, errorNotice, message = '', customizeSetting; + + if ( 1 === errorAnnotations.length ) { + message = component.l10n.errorNotice.singular.replace( '%d', '1' ); + } else if ( errorAnnotations.length > 1 ) { + message = component.l10n.errorNotice.plural.replace( '%d', String( errorAnnotations.length ) ); + } + + if ( control.fields.content[0].setCustomValidity ) { + control.fields.content[0].setCustomValidity( message ); + } + + if ( wp.customize && wp.customize.has( control.customizeSettingId ) ) { + customizeSetting = wp.customize( control.customizeSettingId ); + customizeSetting.notifications.remove( 'htmlhint_error' ); + if ( 0 !== errorAnnotations.length ) { + customizeSetting.notifications.add( 'htmlhint_error', new wp.customize.Notification( 'htmlhint_error', { + message: message, + type: 'error' + } ) ); + } + } else if ( 0 !== errorAnnotations.length ) { + errorNotice = $( '<div class="inline notice notice-error notice-alt"></div>' ); + errorNotice.append( $( '<p></p>', { + text: message + } ) ); + control.errorNoticeContainer.empty(); + control.errorNoticeContainer.append( errorNotice ); + control.errorNoticeContainer.slideDown( 'fast' ); + wp.a11y.speak( message ); + } else { + control.errorNoticeContainer.slideUp( 'fast' ); + } + }, + + /** + * Initialize editor. + * + * @return {void} + */ + initializeEditor: function initializeEditor() { + var control = this, settings; + + if ( component.codeEditorSettings.disabled ) { + return; + } + + settings = _.extend( {}, component.codeEditorSettings, { + + /** + * Handle tabbing to the field before the editor. + * + * @ignore + * + * @return {void} + */ + onTabPrevious: function onTabPrevious() { + control.fields.title.focus(); + }, + + /** + * Handle tabbing to the field after the editor. + * + * @ignore + * + * @return {void} + */ + onTabNext: function onTabNext() { + var tabbables = control.syncContainer.add( control.syncContainer.parent().find( '.widget-position, .widget-control-actions' ) ).find( ':tabbable' ); + tabbables.first().focus(); + }, + + /** + * Disable save button and store linting errors for use in updateFields. + * + * @ignore + * + * @param {Array} errorAnnotations - Error notifications. + * @return {void} + */ + onChangeLintingErrors: function onChangeLintingErrors( errorAnnotations ) { + control.currentErrorAnnotations = errorAnnotations; + }, + + /** + * Update error notice. + * + * @ignore + * + * @param {Array} errorAnnotations - Error annotations. + * @return {void} + */ + onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) { + control.saveButton.toggleClass( 'validation-blocked disabled', errorAnnotations.length > 0 ); + control.updateErrorNotice( errorAnnotations ); + } + }); + + control.editor = wp.codeEditor.initialize( control.fields.content, settings ); + + // Improve the editor accessibility. + $( control.editor.codemirror.display.lineDiv ) + .attr({ + role: 'textbox', + 'aria-multiline': 'true', + 'aria-labelledby': control.fields.content[0].id + '-label', + 'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4' + }); + + // Focus the editor when clicking on its label. + $( '#' + control.fields.content[0].id + '-label' ).on( 'click', function() { + control.editor.codemirror.focus(); + }); + + control.fields.content.on( 'change', function() { + if ( this.value !== control.editor.codemirror.getValue() ) { + control.editor.codemirror.setValue( this.value ); + } + }); + control.editor.codemirror.on( 'change', function() { + var value = control.editor.codemirror.getValue(); + if ( value !== control.fields.content.val() ) { + control.fields.content.val( value ).trigger( 'change' ); + } + }); + + // Make sure the editor gets updated if the content was updated on the server (sanitization) but not updated in the editor since it was focused. + control.editor.codemirror.on( 'blur', function() { + if ( control.contentUpdateBypassed ) { + control.syncContainer.find( '.sync-input.content' ).trigger( 'change' ); + } + }); + + // Prevent hitting Esc from collapsing the widget control. + if ( wp.customize ) { + control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) { + var escKeyCode = 27; + if ( escKeyCode === event.keyCode ) { + event.stopPropagation(); + } + }); + } + } + }); + + /** + * Mapping of widget ID to instances of CustomHtmlWidgetControl subclasses. + * + * @alias wp.customHtmlWidgets.widgetControls + * + * @type {Object.<string, wp.textWidgets.CustomHtmlWidgetControl>} + */ + component.widgetControls = {}; + + /** + * Handle widget being added or initialized for the first time at the widget-added event. + * + * @alias wp.customHtmlWidgets.handleWidgetAdded + * + * @param {jQuery.Event} event - Event. + * @param {jQuery} widgetContainer - Widget container element. + * + * @return {void} + */ + component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { + var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer; + widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen. + + idBase = widgetForm.find( '> .id_base' ).val(); + if ( -1 === component.idBases.indexOf( idBase ) ) { + return; + } + + // Prevent initializing already-added widgets. + widgetId = widgetForm.find( '.widget-id' ).val(); + if ( component.widgetControls[ widgetId ] ) { + return; + } + + /* + * Create a container element for the widget control fields. + * This is inserted into the DOM immediately before the the .widget-content + * element because the contents of this element are essentially "managed" + * by PHP, where each widget update cause the entire element to be emptied + * and replaced with the rendered output of WP_Widget::form() which is + * sent back in Ajax request made to save/update the widget instance. + * To prevent a "flash of replaced DOM elements and re-initialized JS + * components", the JS template is rendered outside of the normal form + * container. + */ + fieldContainer = $( '<div></div>' ); + syncContainer = widgetContainer.find( '.widget-content:first' ); + syncContainer.before( fieldContainer ); + + widgetControl = new component.CustomHtmlWidgetControl({ + el: fieldContainer, + syncContainer: syncContainer + }); + + component.widgetControls[ widgetId ] = widgetControl; + + /* + * Render the widget once the widget parent's container finishes animating, + * as the widget-added event fires with a slideDown of the container. + * This ensures that the textarea is visible and the editor can be initialized. + */ + renderWhenAnimationDone = function() { + if ( ! ( wp.customize ? widgetContainer.parent().hasClass( 'expanded' ) : widgetContainer.hasClass( 'open' ) ) ) { // Core merge: The wp.customize condition can be eliminated with this change being in core: https://github.com/xwp/wordpress-develop/pull/247/commits/5322387d + setTimeout( renderWhenAnimationDone, animatedCheckDelay ); + } else { + widgetControl.initializeEditor(); + } + }; + renderWhenAnimationDone(); + }; + + /** + * Setup widget in accessibility mode. + * + * @alias wp.customHtmlWidgets.setupAccessibleMode + * + * @return {void} + */ + component.setupAccessibleMode = function setupAccessibleMode() { + var widgetForm, idBase, widgetControl, fieldContainer, syncContainer; + widgetForm = $( '.editwidget > form' ); + if ( 0 === widgetForm.length ) { + return; + } + + idBase = widgetForm.find( '.id_base' ).val(); + if ( -1 === component.idBases.indexOf( idBase ) ) { + return; + } + + fieldContainer = $( '<div></div>' ); + syncContainer = widgetForm.find( '> .widget-inside' ); + syncContainer.before( fieldContainer ); + + widgetControl = new component.CustomHtmlWidgetControl({ + el: fieldContainer, + syncContainer: syncContainer + }); + + widgetControl.initializeEditor(); + }; + + /** + * Sync widget instance data sanitized from server back onto widget model. + * + * This gets called via the 'widget-updated' event when saving a widget from + * the widgets admin screen and also via the 'widget-synced' event when making + * a change to a widget in the customizer. + * + * @alias wp.customHtmlWidgets.handleWidgetUpdated + * + * @param {jQuery.Event} event - Event. + * @param {jQuery} widgetContainer - Widget container element. + * @return {void} + */ + component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) { + var widgetForm, widgetId, widgetControl, idBase; + widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); + + idBase = widgetForm.find( '> .id_base' ).val(); + if ( -1 === component.idBases.indexOf( idBase ) ) { + return; + } + + widgetId = widgetForm.find( '> .widget-id' ).val(); + widgetControl = component.widgetControls[ widgetId ]; + if ( ! widgetControl ) { + return; + } + + widgetControl.updateFields(); + }; + + /** + * Initialize functionality. + * + * This function exists to prevent the JS file from having to boot itself. + * When WordPress enqueues this script, it should have an inline script + * attached which calls wp.textWidgets.init(). + * + * @alias wp.customHtmlWidgets.init + * + * @param {Object} settings - Options for code editor, exported from PHP. + * + * @return {void} + */ + component.init = function init( settings ) { + var $document = $( document ); + _.extend( component.codeEditorSettings, settings ); + + $document.on( 'widget-added', component.handleWidgetAdded ); + $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); + + /* + * Manually trigger widget-added events for media widgets on the admin + * screen once they are expanded. The widget-added event is not triggered + * for each pre-existing widget on the widgets admin screen like it is + * on the customizer. Likewise, the customizer only triggers widget-added + * when the widget is expanded to just-in-time construct the widget form + * when it is actually going to be displayed. So the following implements + * the same for the widgets admin screen, to invoke the widget-added + * handler when a pre-existing media widget is expanded. + */ + $( function initializeExistingWidgetContainers() { + var widgetContainers; + if ( 'widgets' !== window.pagenow ) { + return; + } + widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' ); + widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() { + var widgetContainer = $( this ); + component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); + }); + + // Accessibility mode. + if ( document.readyState === 'complete' ) { + // Page is fully loaded. + component.setupAccessibleMode(); + } else { + // Page is still loading. + $( window ).on( 'load', function() { + component.setupAccessibleMode(); + }); + } + }); + }; + + return component; +})( jQuery ); diff --git a/wp-admin/js/widgets/custom-html-widgets.min.js b/wp-admin/js/widgets/custom-html-widgets.min.js new file mode 100644 index 0000000..8baa98f --- /dev/null +++ b/wp-admin/js/widgets/custom-html-widgets.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +wp.customHtmlWidgets=function(a){"use strict";var s={idBases:["custom_html"],codeEditorSettings:{},l10n:{errorNotice:{singular:"",plural:""}}};return s.CustomHtmlWidgetControl=Backbone.View.extend({events:{},initialize:function(e){var n=this;if(!e.el)throw new Error("Missing options.el");if(!e.syncContainer)throw new Error("Missing options.syncContainer");Backbone.View.prototype.initialize.call(n,e),n.syncContainer=e.syncContainer,n.widgetIdBase=n.syncContainer.parent().find(".id_base").val(),n.widgetNumber=n.syncContainer.parent().find(".widget_number").val(),n.customizeSettingId="widget_"+n.widgetIdBase+"["+String(n.widgetNumber)+"]",n.$el.addClass("custom-html-widget-fields"),n.$el.html(wp.template("widget-custom-html-control-fields")({codeEditorDisabled:s.codeEditorSettings.disabled})),n.errorNoticeContainer=n.$el.find(".code-editor-error-container"),n.currentErrorAnnotations=[],n.saveButton=n.syncContainer.add(n.syncContainer.parent().find(".widget-control-actions")).find(".widget-control-save, #savewidget"),n.saveButton.addClass("custom-html-widget-save-button"),n.fields={title:n.$el.find(".title"),content:n.$el.find(".content")},_.each(n.fields,function(t,i){t.on("input change",function(){var e=n.syncContainer.find(".sync-input."+i);e.val()!==t.val()&&(e.val(t.val()),e.trigger("change"))}),t.val(n.syncContainer.find(".sync-input."+i).val())})},updateFields:function(){var e,t=this;t.fields.title.is(document.activeElement)||(e=t.syncContainer.find(".sync-input.title"),t.fields.title.val(e.val())),t.contentUpdateBypassed=t.fields.content.is(document.activeElement)||t.editor&&t.editor.codemirror.state.focused||0!==t.currentErrorAnnotations.length,t.contentUpdateBypassed||(e=t.syncContainer.find(".sync-input.content"),t.fields.content.val(e.val()))},updateErrorNotice:function(e){var t,i=this,n="";1===e.length?n=s.l10n.errorNotice.singular.replace("%d","1"):1<e.length&&(n=s.l10n.errorNotice.plural.replace("%d",String(e.length))),i.fields.content[0].setCustomValidity&&i.fields.content[0].setCustomValidity(n),wp.customize&&wp.customize.has(i.customizeSettingId)?((t=wp.customize(i.customizeSettingId)).notifications.remove("htmlhint_error"),0!==e.length&&t.notifications.add("htmlhint_error",new wp.customize.Notification("htmlhint_error",{message:n,type:"error"}))):0!==e.length?((t=a('<div class="inline notice notice-error notice-alt"></div>')).append(a("<p></p>",{text:n})),i.errorNoticeContainer.empty(),i.errorNoticeContainer.append(t),i.errorNoticeContainer.slideDown("fast"),wp.a11y.speak(n)):i.errorNoticeContainer.slideUp("fast")},initializeEditor:function(){var e,t=this;s.codeEditorSettings.disabled||(e=_.extend({},s.codeEditorSettings,{onTabPrevious:function(){t.fields.title.focus()},onTabNext:function(){t.syncContainer.add(t.syncContainer.parent().find(".widget-position, .widget-control-actions")).find(":tabbable").first().focus()},onChangeLintingErrors:function(e){t.currentErrorAnnotations=e},onUpdateErrorNotice:function(e){t.saveButton.toggleClass("validation-blocked disabled",0<e.length),t.updateErrorNotice(e)}}),t.editor=wp.codeEditor.initialize(t.fields.content,e),a(t.editor.codemirror.display.lineDiv).attr({role:"textbox","aria-multiline":"true","aria-labelledby":t.fields.content[0].id+"-label","aria-describedby":"editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4"}),a("#"+t.fields.content[0].id+"-label").on("click",function(){t.editor.codemirror.focus()}),t.fields.content.on("change",function(){this.value!==t.editor.codemirror.getValue()&&t.editor.codemirror.setValue(this.value)}),t.editor.codemirror.on("change",function(){var e=t.editor.codemirror.getValue();e!==t.fields.content.val()&&t.fields.content.val(e).trigger("change")}),t.editor.codemirror.on("blur",function(){t.contentUpdateBypassed&&t.syncContainer.find(".sync-input.content").trigger("change")}),wp.customize&&t.editor.codemirror.on("keydown",function(e,t){27===t.keyCode&&t.stopPropagation()}))}}),s.widgetControls={},s.handleWidgetAdded=function(e,t){var i,n,o,d=t.find("> .widget-inside > .form, > .widget-inside > form"),r=d.find("> .id_base").val();-1===s.idBases.indexOf(r)||(r=d.find(".widget-id").val(),s.widgetControls[r])||(d=a("<div></div>"),(o=t.find(".widget-content:first")).before(d),i=new s.CustomHtmlWidgetControl({el:d,syncContainer:o}),s.widgetControls[r]=i,(n=function(){(wp.customize?t.parent().hasClass("expanded"):t.hasClass("open"))?i.initializeEditor():setTimeout(n,50)})())},s.setupAccessibleMode=function(){var e,t=a(".editwidget > form");0!==t.length&&(e=t.find(".id_base").val(),-1!==s.idBases.indexOf(e))&&(e=a("<div></div>"),(t=t.find("> .widget-inside")).before(e),new s.CustomHtmlWidgetControl({el:e,syncContainer:t}).initializeEditor())},s.handleWidgetUpdated=function(e,t){var t=t.find("> .widget-inside > .form, > .widget-inside > form"),i=t.find("> .id_base").val();-1!==s.idBases.indexOf(i)&&(i=t.find("> .widget-id").val(),t=s.widgetControls[i])&&t.updateFields()},s.init=function(e){var t=a(document);_.extend(s.codeEditorSettings,e),t.on("widget-added",s.handleWidgetAdded),t.on("widget-synced widget-updated",s.handleWidgetUpdated),a(function(){"widgets"===window.pagenow&&(a(".widgets-holder-wrap:not(#available-widgets)").find("div.widget").one("click.toggle-widget-expanded",function(){var e=a(this);s.handleWidgetAdded(new jQuery.Event("widget-added"),e)}),"complete"===document.readyState?s.setupAccessibleMode():a(window).on("load",function(){s.setupAccessibleMode()}))})},s}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/widgets/media-audio-widget.js b/wp-admin/js/widgets/media-audio-widget.js new file mode 100644 index 0000000..a579253 --- /dev/null +++ b/wp-admin/js/widgets/media-audio-widget.js @@ -0,0 +1,154 @@ +/** + * @output wp-admin/js/widgets/media-audio-widget.js + */ + +/* eslint consistent-this: [ "error", "control" ] */ +(function( component ) { + 'use strict'; + + var AudioWidgetModel, AudioWidgetControl, AudioDetailsMediaFrame; + + /** + * Custom audio details frame that removes the replace-audio state. + * + * @class wp.mediaWidgets.controlConstructors~AudioDetailsMediaFrame + * @augments wp.media.view.MediaFrame.AudioDetails + */ + AudioDetailsMediaFrame = wp.media.view.MediaFrame.AudioDetails.extend(/** @lends wp.mediaWidgets.controlConstructors~AudioDetailsMediaFrame.prototype */{ + + /** + * Create the default states. + * + * @return {void} + */ + createStates: function createStates() { + this.states.add([ + new wp.media.controller.AudioDetails({ + media: this.media + }), + + new wp.media.controller.MediaLibrary({ + type: 'audio', + id: 'add-audio-source', + title: wp.media.view.l10n.audioAddSourceTitle, + toolbar: 'add-audio-source', + media: this.media, + menu: false + }) + ]); + } + }); + + /** + * Audio widget model. + * + * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @class wp.mediaWidgets.modelConstructors.media_audio + * @augments wp.mediaWidgets.MediaWidgetModel + */ + AudioWidgetModel = component.MediaWidgetModel.extend({}); + + /** + * Audio widget control. + * + * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @class wp.mediaWidgets.controlConstructors.media_audio + * @augments wp.mediaWidgets.MediaWidgetControl + */ + AudioWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_audio.prototype */{ + + /** + * Show display settings. + * + * @type {boolean} + */ + showDisplaySettings: false, + + /** + * Map model props to media frame props. + * + * @param {Object} modelProps - Model props. + * @return {Object} Media frame props. + */ + mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) { + var control = this, mediaFrameProps; + mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps ); + mediaFrameProps.link = 'embed'; + return mediaFrameProps; + }, + + /** + * Render preview. + * + * @return {void} + */ + renderPreview: function renderPreview() { + var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl; + attachmentId = control.model.get( 'attachment_id' ); + attachmentUrl = control.model.get( 'url' ); + + if ( ! attachmentId && ! attachmentUrl ) { + return; + } + + previewContainer = control.$el.find( '.media-widget-preview' ); + previewTemplate = wp.template( 'wp-media-widget-audio-preview' ); + + previewContainer.html( previewTemplate({ + model: { + attachment_id: control.model.get( 'attachment_id' ), + src: attachmentUrl + }, + error: control.model.get( 'error' ) + })); + wp.mediaelement.initialize(); + }, + + /** + * Open the media audio-edit frame to modify the selected item. + * + * @return {void} + */ + editMedia: function editMedia() { + var control = this, mediaFrame, metadata, updateCallback; + + metadata = control.mapModelToMediaFrameProps( control.model.toJSON() ); + + // Set up the media frame. + mediaFrame = new AudioDetailsMediaFrame({ + frame: 'audio', + state: 'audio-details', + metadata: metadata + }); + wp.media.frame = mediaFrame; + mediaFrame.$el.addClass( 'media-widget' ); + + updateCallback = function( mediaFrameProps ) { + + // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. + control.selectedAttachment.set( mediaFrameProps ); + + control.model.set( _.extend( + control.model.defaults(), + control.mapMediaToModelProps( mediaFrameProps ), + { error: false } + ) ); + }; + + mediaFrame.state( 'audio-details' ).on( 'update', updateCallback ); + mediaFrame.state( 'replace-audio' ).on( 'replace', updateCallback ); + mediaFrame.on( 'close', function() { + mediaFrame.detach(); + }); + + mediaFrame.open(); + } + }); + + // Exports. + component.controlConstructors.media_audio = AudioWidgetControl; + component.modelConstructors.media_audio = AudioWidgetModel; + +})( wp.mediaWidgets ); diff --git a/wp-admin/js/widgets/media-audio-widget.min.js b/wp-admin/js/widgets/media-audio-widget.min.js new file mode 100644 index 0000000..e26f4f8 --- /dev/null +++ b/wp-admin/js/widgets/media-audio-widget.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(t){"use strict";var a=wp.media.view.MediaFrame.AudioDetails.extend({createStates:function(){this.states.add([new wp.media.controller.AudioDetails({media:this.media}),new wp.media.controller.MediaLibrary({type:"audio",id:"add-audio-source",title:wp.media.view.l10n.audioAddSourceTitle,toolbar:"add-audio-source",media:this.media,menu:!1})])}}),e=t.MediaWidgetModel.extend({}),d=t.MediaWidgetControl.extend({showDisplaySettings:!1,mapModelToMediaFrameProps:function(e){e=t.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call(this,e);return e.link="embed",e},renderPreview:function(){var e,t=this,d=t.model.get("attachment_id"),a=t.model.get("url");(d||a)&&(d=t.$el.find(".media-widget-preview"),e=wp.template("wp-media-widget-audio-preview"),d.html(e({model:{attachment_id:t.model.get("attachment_id"),src:a},error:t.model.get("error")})),wp.mediaelement.initialize())},editMedia:function(){var t=this,e=t.mapModelToMediaFrameProps(t.model.toJSON()),d=new a({frame:"audio",state:"audio-details",metadata:e});(wp.media.frame=d).$el.addClass("media-widget"),e=function(e){t.selectedAttachment.set(e),t.model.set(_.extend(t.model.defaults(),t.mapMediaToModelProps(e),{error:!1}))},d.state("audio-details").on("update",e),d.state("replace-audio").on("replace",e),d.on("close",function(){d.detach()}),d.open()}});t.controlConstructors.media_audio=d,t.modelConstructors.media_audio=e}(wp.mediaWidgets);
\ No newline at end of file diff --git a/wp-admin/js/widgets/media-gallery-widget.js b/wp-admin/js/widgets/media-gallery-widget.js new file mode 100644 index 0000000..020e978 --- /dev/null +++ b/wp-admin/js/widgets/media-gallery-widget.js @@ -0,0 +1,341 @@ +/** + * @output wp-admin/js/widgets/media-gallery-widget.js + */ + +/* eslint consistent-this: [ "error", "control" ] */ +(function( component ) { + 'use strict'; + + var GalleryWidgetModel, GalleryWidgetControl, GalleryDetailsMediaFrame; + + /** + * Custom gallery details frame. + * + * @since 4.9.0 + * @class wp.mediaWidgets~GalleryDetailsMediaFrame + * @augments wp.media.view.MediaFrame.Post + */ + GalleryDetailsMediaFrame = wp.media.view.MediaFrame.Post.extend(/** @lends wp.mediaWidgets~GalleryDetailsMediaFrame.prototype */{ + + /** + * Create the default states. + * + * @since 4.9.0 + * @return {void} + */ + createStates: function createStates() { + this.states.add([ + new wp.media.controller.Library({ + id: 'gallery', + title: wp.media.view.l10n.createGalleryTitle, + priority: 40, + toolbar: 'main-gallery', + filterable: 'uploaded', + multiple: 'add', + editable: true, + + library: wp.media.query( _.defaults({ + type: 'image' + }, this.options.library ) ) + }), + + // Gallery states. + new wp.media.controller.GalleryEdit({ + library: this.options.selection, + editing: this.options.editing, + menu: 'gallery' + }), + + new wp.media.controller.GalleryAdd() + ]); + } + } ); + + /** + * Gallery widget model. + * + * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @since 4.9.0 + * + * @class wp.mediaWidgets.modelConstructors.media_gallery + * @augments wp.mediaWidgets.MediaWidgetModel + */ + GalleryWidgetModel = component.MediaWidgetModel.extend(/** @lends wp.mediaWidgets.modelConstructors.media_gallery.prototype */{} ); + + GalleryWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_gallery.prototype */{ + + /** + * View events. + * + * @since 4.9.0 + * @type {object} + */ + events: _.extend( {}, component.MediaWidgetControl.prototype.events, { + 'click .media-widget-gallery-preview': 'editMedia' + } ), + + /** + * Gallery widget control. + * + * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @constructs wp.mediaWidgets.controlConstructors.media_gallery + * @augments wp.mediaWidgets.MediaWidgetControl + * + * @since 4.9.0 + * @param {Object} options - Options. + * @param {Backbone.Model} options.model - Model. + * @param {jQuery} options.el - Control field container element. + * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. + * @return {void} + */ + initialize: function initialize( options ) { + var control = this; + + component.MediaWidgetControl.prototype.initialize.call( control, options ); + + _.bindAll( control, 'updateSelectedAttachments', 'handleAttachmentDestroy' ); + control.selectedAttachments = new wp.media.model.Attachments(); + control.model.on( 'change:ids', control.updateSelectedAttachments ); + control.selectedAttachments.on( 'change', control.renderPreview ); + control.selectedAttachments.on( 'reset', control.renderPreview ); + control.updateSelectedAttachments(); + + /* + * Refresh a Gallery widget partial when the user modifies one of the selected attachments. + * This ensures that when an attachment's caption is updated in the media modal the Gallery + * widget in the preview will then be refreshed to show the change. Normally doing this + * would not be necessary because all of the state should be contained inside the changeset, + * as everything done in the Customizer should not make a change to the site unless the + * changeset itself is published. Attachments are a current exception to this rule. + * For a proposal to include attachments in the customized state, see #37887. + */ + if ( wp.customize && wp.customize.previewer ) { + control.selectedAttachments.on( 'change', function() { + wp.customize.previewer.send( 'refresh-widget-partial', control.model.get( 'widget_id' ) ); + } ); + } + }, + + /** + * Update the selected attachments if necessary. + * + * @since 4.9.0 + * @return {void} + */ + updateSelectedAttachments: function updateSelectedAttachments() { + var control = this, newIds, oldIds, removedIds, addedIds, addedQuery; + + newIds = control.model.get( 'ids' ); + oldIds = _.pluck( control.selectedAttachments.models, 'id' ); + + removedIds = _.difference( oldIds, newIds ); + _.each( removedIds, function( removedId ) { + control.selectedAttachments.remove( control.selectedAttachments.get( removedId ) ); + }); + + addedIds = _.difference( newIds, oldIds ); + if ( addedIds.length ) { + addedQuery = wp.media.query({ + order: 'ASC', + orderby: 'post__in', + perPage: -1, + post__in: newIds, + query: true, + type: 'image' + }); + addedQuery.more().done( function() { + control.selectedAttachments.reset( addedQuery.models ); + }); + } + }, + + /** + * Render preview. + * + * @since 4.9.0 + * @return {void} + */ + renderPreview: function renderPreview() { + var control = this, previewContainer, previewTemplate, data; + + previewContainer = control.$el.find( '.media-widget-preview' ); + previewTemplate = wp.template( 'wp-media-widget-gallery-preview' ); + + data = control.previewTemplateProps.toJSON(); + data.attachments = {}; + control.selectedAttachments.each( function( attachment ) { + data.attachments[ attachment.id ] = attachment.toJSON(); + } ); + + previewContainer.html( previewTemplate( data ) ); + }, + + /** + * Determine whether there are selected attachments. + * + * @since 4.9.0 + * @return {boolean} Selected. + */ + isSelected: function isSelected() { + var control = this; + + if ( control.model.get( 'error' ) ) { + return false; + } + + return control.model.get( 'ids' ).length > 0; + }, + + /** + * Open the media select frame to edit images. + * + * @since 4.9.0 + * @return {void} + */ + editMedia: function editMedia() { + var control = this, selection, mediaFrame, mediaFrameProps; + + selection = new wp.media.model.Selection( control.selectedAttachments.models, { + multiple: true + }); + + mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() ); + selection.gallery = new Backbone.Model( mediaFrameProps ); + if ( mediaFrameProps.size ) { + control.displaySettings.set( 'size', mediaFrameProps.size ); + } + mediaFrame = new GalleryDetailsMediaFrame({ + frame: 'manage', + text: control.l10n.add_to_widget, + selection: selection, + mimeType: control.mime_type, + selectedDisplaySettings: control.displaySettings, + showDisplaySettings: control.showDisplaySettings, + metadata: mediaFrameProps, + editing: true, + multiple: true, + state: 'gallery-edit' + }); + wp.media.frame = mediaFrame; // See wp.media(). + + // Handle selection of a media item. + mediaFrame.on( 'update', function onUpdate( newSelection ) { + var state = mediaFrame.state(), resultSelection; + + resultSelection = newSelection || state.get( 'selection' ); + if ( ! resultSelection ) { + return; + } + + // Copy orderby_random from gallery state. + if ( resultSelection.gallery ) { + control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) ); + } + + // Directly update selectedAttachments to prevent needing to do additional request. + control.selectedAttachments.reset( resultSelection.models ); + + // Update models in the widget instance. + control.model.set( { + ids: _.pluck( resultSelection.models, 'id' ) + } ); + } ); + + mediaFrame.$el.addClass( 'media-widget' ); + mediaFrame.open(); + + if ( selection ) { + selection.on( 'destroy', control.handleAttachmentDestroy ); + } + }, + + /** + * Open the media select frame to chose an item. + * + * @since 4.9.0 + * @return {void} + */ + selectMedia: function selectMedia() { + var control = this, selection, mediaFrame, mediaFrameProps; + selection = new wp.media.model.Selection( control.selectedAttachments.models, { + multiple: true + }); + + mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() ); + if ( mediaFrameProps.size ) { + control.displaySettings.set( 'size', mediaFrameProps.size ); + } + mediaFrame = new GalleryDetailsMediaFrame({ + frame: 'select', + text: control.l10n.add_to_widget, + selection: selection, + mimeType: control.mime_type, + selectedDisplaySettings: control.displaySettings, + showDisplaySettings: control.showDisplaySettings, + metadata: mediaFrameProps, + state: 'gallery' + }); + wp.media.frame = mediaFrame; // See wp.media(). + + // Handle selection of a media item. + mediaFrame.on( 'update', function onUpdate( newSelection ) { + var state = mediaFrame.state(), resultSelection; + + resultSelection = newSelection || state.get( 'selection' ); + if ( ! resultSelection ) { + return; + } + + // Copy orderby_random from gallery state. + if ( resultSelection.gallery ) { + control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) ); + } + + // Directly update selectedAttachments to prevent needing to do additional request. + control.selectedAttachments.reset( resultSelection.models ); + + // Update widget instance. + control.model.set( { + ids: _.pluck( resultSelection.models, 'id' ) + } ); + } ); + + mediaFrame.$el.addClass( 'media-widget' ); + mediaFrame.open(); + + if ( selection ) { + selection.on( 'destroy', control.handleAttachmentDestroy ); + } + + /* + * Make sure focus is set inside of modal so that hitting Esc will close + * the modal and not inadvertently cause the widget to collapse in the customizer. + */ + mediaFrame.$el.find( ':focusable:first' ).focus(); + }, + + /** + * Clear the selected attachment when it is deleted in the media select frame. + * + * @since 4.9.0 + * @param {wp.media.models.Attachment} attachment - Attachment. + * @return {void} + */ + handleAttachmentDestroy: function handleAttachmentDestroy( attachment ) { + var control = this; + control.model.set( { + ids: _.difference( + control.model.get( 'ids' ), + [ attachment.id ] + ) + } ); + } + } ); + + // Exports. + component.controlConstructors.media_gallery = GalleryWidgetControl; + component.modelConstructors.media_gallery = GalleryWidgetModel; + +})( wp.mediaWidgets ); diff --git a/wp-admin/js/widgets/media-gallery-widget.min.js b/wp-admin/js/widgets/media-gallery-widget.min.js new file mode 100644 index 0000000..69734a9 --- /dev/null +++ b/wp-admin/js/widgets/media-gallery-widget.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(i){"use strict";var a=wp.media.view.MediaFrame.Post.extend({createStates:function(){this.states.add([new wp.media.controller.Library({id:"gallery",title:wp.media.view.l10n.createGalleryTitle,priority:40,toolbar:"main-gallery",filterable:"uploaded",multiple:"add",editable:!0,library:wp.media.query(_.defaults({type:"image"},this.options.library))}),new wp.media.controller.GalleryEdit({library:this.options.selection,editing:this.options.editing,menu:"gallery"}),new wp.media.controller.GalleryAdd])}}),e=i.MediaWidgetModel.extend({}),t=i.MediaWidgetControl.extend({events:_.extend({},i.MediaWidgetControl.prototype.events,{"click .media-widget-gallery-preview":"editMedia"}),initialize:function(e){var t=this;i.MediaWidgetControl.prototype.initialize.call(t,e),_.bindAll(t,"updateSelectedAttachments","handleAttachmentDestroy"),t.selectedAttachments=new wp.media.model.Attachments,t.model.on("change:ids",t.updateSelectedAttachments),t.selectedAttachments.on("change",t.renderPreview),t.selectedAttachments.on("reset",t.renderPreview),t.updateSelectedAttachments(),wp.customize&&wp.customize.previewer&&t.selectedAttachments.on("change",function(){wp.customize.previewer.send("refresh-widget-partial",t.model.get("widget_id"))})},updateSelectedAttachments:function(){var e,t=this,i=t.model.get("ids"),d=_.pluck(t.selectedAttachments.models,"id"),a=_.difference(d,i);_.each(a,function(e){t.selectedAttachments.remove(t.selectedAttachments.get(e))}),_.difference(i,d).length&&(e=wp.media.query({order:"ASC",orderby:"post__in",perPage:-1,post__in:i,query:!0,type:"image"})).more().done(function(){t.selectedAttachments.reset(e.models)})},renderPreview:function(){var e=this,t=e.$el.find(".media-widget-preview"),i=wp.template("wp-media-widget-gallery-preview"),d=e.previewTemplateProps.toJSON();d.attachments={},e.selectedAttachments.each(function(e){d.attachments[e.id]=e.toJSON()}),t.html(i(d))},isSelected:function(){return!this.model.get("error")&&0<this.model.get("ids").length},editMedia:function(){var i,d=this,e=new wp.media.model.Selection(d.selectedAttachments.models,{multiple:!0}),t=d.mapModelToMediaFrameProps(d.model.toJSON());e.gallery=new Backbone.Model(t),t.size&&d.displaySettings.set("size",t.size),i=new a({frame:"manage",text:d.l10n.add_to_widget,selection:e,mimeType:d.mime_type,selectedDisplaySettings:d.displaySettings,showDisplaySettings:d.showDisplaySettings,metadata:t,editing:!0,multiple:!0,state:"gallery-edit"}),(wp.media.frame=i).on("update",function(e){var t=i.state(),e=e||t.get("selection");e&&(e.gallery&&d.model.set(d.mapMediaToModelProps(e.gallery.toJSON())),d.selectedAttachments.reset(e.models),d.model.set({ids:_.pluck(e.models,"id")}))}),i.$el.addClass("media-widget"),i.open(),e&&e.on("destroy",d.handleAttachmentDestroy)},selectMedia:function(){var i,d=this,e=new wp.media.model.Selection(d.selectedAttachments.models,{multiple:!0}),t=d.mapModelToMediaFrameProps(d.model.toJSON());t.size&&d.displaySettings.set("size",t.size),i=new a({frame:"select",text:d.l10n.add_to_widget,selection:e,mimeType:d.mime_type,selectedDisplaySettings:d.displaySettings,showDisplaySettings:d.showDisplaySettings,metadata:t,state:"gallery"}),(wp.media.frame=i).on("update",function(e){var t=i.state(),e=e||t.get("selection");e&&(e.gallery&&d.model.set(d.mapMediaToModelProps(e.gallery.toJSON())),d.selectedAttachments.reset(e.models),d.model.set({ids:_.pluck(e.models,"id")}))}),i.$el.addClass("media-widget"),i.open(),e&&e.on("destroy",d.handleAttachmentDestroy),i.$el.find(":focusable:first").focus()},handleAttachmentDestroy:function(e){this.model.set({ids:_.difference(this.model.get("ids"),[e.id])})}});i.controlConstructors.media_gallery=t,i.modelConstructors.media_gallery=e}(wp.mediaWidgets);
\ No newline at end of file diff --git a/wp-admin/js/widgets/media-image-widget.js b/wp-admin/js/widgets/media-image-widget.js new file mode 100644 index 0000000..7d15eff --- /dev/null +++ b/wp-admin/js/widgets/media-image-widget.js @@ -0,0 +1,170 @@ +/** + * @output wp-admin/js/widgets/media-image-widget.js + */ + +/* eslint consistent-this: [ "error", "control" ] */ +(function( component, $ ) { + 'use strict'; + + var ImageWidgetModel, ImageWidgetControl; + + /** + * Image widget model. + * + * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @class wp.mediaWidgets.modelConstructors.media_image + * @augments wp.mediaWidgets.MediaWidgetModel + */ + ImageWidgetModel = component.MediaWidgetModel.extend({}); + + /** + * Image widget control. + * + * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @class wp.mediaWidgets.controlConstructors.media_audio + * @augments wp.mediaWidgets.MediaWidgetControl + */ + ImageWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_image.prototype */{ + + /** + * View events. + * + * @type {object} + */ + events: _.extend( {}, component.MediaWidgetControl.prototype.events, { + 'click .media-widget-preview.populated': 'editMedia' + } ), + + /** + * Render preview. + * + * @return {void} + */ + renderPreview: function renderPreview() { + var control = this, previewContainer, previewTemplate, fieldsContainer, fieldsTemplate, linkInput; + if ( ! control.model.get( 'attachment_id' ) && ! control.model.get( 'url' ) ) { + return; + } + + previewContainer = control.$el.find( '.media-widget-preview' ); + previewTemplate = wp.template( 'wp-media-widget-image-preview' ); + previewContainer.html( previewTemplate( control.previewTemplateProps.toJSON() ) ); + previewContainer.addClass( 'populated' ); + + linkInput = control.$el.find( '.link' ); + if ( ! linkInput.is( document.activeElement ) ) { + fieldsContainer = control.$el.find( '.media-widget-fields' ); + fieldsTemplate = wp.template( 'wp-media-widget-image-fields' ); + fieldsContainer.html( fieldsTemplate( control.previewTemplateProps.toJSON() ) ); + } + }, + + /** + * Open the media image-edit frame to modify the selected item. + * + * @return {void} + */ + editMedia: function editMedia() { + var control = this, mediaFrame, updateCallback, defaultSync, metadata; + + metadata = control.mapModelToMediaFrameProps( control.model.toJSON() ); + + // Needed or else none will not be selected if linkUrl is not also empty. + if ( 'none' === metadata.link ) { + metadata.linkUrl = ''; + } + + // Set up the media frame. + mediaFrame = wp.media({ + frame: 'image', + state: 'image-details', + metadata: metadata + }); + mediaFrame.$el.addClass( 'media-widget' ); + + updateCallback = function() { + var mediaProps, linkType; + + // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. + mediaProps = mediaFrame.state().attributes.image.toJSON(); + linkType = mediaProps.link; + mediaProps.link = mediaProps.linkUrl; + control.selectedAttachment.set( mediaProps ); + control.displaySettings.set( 'link', linkType ); + + control.model.set( _.extend( + control.mapMediaToModelProps( mediaProps ), + { error: false } + ) ); + }; + + mediaFrame.state( 'image-details' ).on( 'update', updateCallback ); + mediaFrame.state( 'replace-image' ).on( 'replace', updateCallback ); + + // Disable syncing of attachment changes back to server. See <https://core.trac.wordpress.org/ticket/40403>. + defaultSync = wp.media.model.Attachment.prototype.sync; + wp.media.model.Attachment.prototype.sync = function rejectedSync() { + return $.Deferred().rejectWith( this ).promise(); + }; + mediaFrame.on( 'close', function onClose() { + mediaFrame.detach(); + wp.media.model.Attachment.prototype.sync = defaultSync; + }); + + mediaFrame.open(); + }, + + /** + * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment). + * + * @return {Object} Reset/override props. + */ + getEmbedResetProps: function getEmbedResetProps() { + return _.extend( + component.MediaWidgetControl.prototype.getEmbedResetProps.call( this ), + { + size: 'full', + width: 0, + height: 0 + } + ); + }, + + /** + * Get the instance props from the media selection frame. + * + * Prevent the image_title attribute from being initially set when adding an image from the media library. + * + * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame. + * @return {Object} Props. + */ + getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) { + var control = this; + return _.omit( + component.MediaWidgetControl.prototype.getModelPropsFromMediaFrame.call( control, mediaFrame ), + 'image_title' + ); + }, + + /** + * Map model props to preview template props. + * + * @return {Object} Preview template props. + */ + mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() { + var control = this, previewTemplateProps, url; + url = control.model.get( 'url' ); + previewTemplateProps = component.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call( control ); + previewTemplateProps.currentFilename = url ? url.replace( /\?.*$/, '' ).replace( /^.+\//, '' ) : ''; + previewTemplateProps.link_url = control.model.get( 'link_url' ); + return previewTemplateProps; + } + }); + + // Exports. + component.controlConstructors.media_image = ImageWidgetControl; + component.modelConstructors.media_image = ImageWidgetModel; + +})( wp.mediaWidgets, jQuery ); diff --git a/wp-admin/js/widgets/media-image-widget.min.js b/wp-admin/js/widgets/media-image-widget.min.js new file mode 100644 index 0000000..fd3f5eb --- /dev/null +++ b/wp-admin/js/widgets/media-image-widget.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(a,o){"use strict";var e=a.MediaWidgetModel.extend({}),t=a.MediaWidgetControl.extend({events:_.extend({},a.MediaWidgetControl.prototype.events,{"click .media-widget-preview.populated":"editMedia"}),renderPreview:function(){var e,t,i=this;(i.model.get("attachment_id")||i.model.get("url"))&&(t=i.$el.find(".media-widget-preview"),e=wp.template("wp-media-widget-image-preview"),t.html(e(i.previewTemplateProps.toJSON())),t.addClass("populated"),i.$el.find(".link").is(document.activeElement)||(e=i.$el.find(".media-widget-fields"),t=wp.template("wp-media-widget-image-fields"),e.html(t(i.previewTemplateProps.toJSON()))))},editMedia:function(){var i,e,a=this,t=a.mapModelToMediaFrameProps(a.model.toJSON());"none"===t.link&&(t.linkUrl=""),(i=wp.media({frame:"image",state:"image-details",metadata:t})).$el.addClass("media-widget"),t=function(){var e=i.state().attributes.image.toJSON(),t=e.link;e.link=e.linkUrl,a.selectedAttachment.set(e),a.displaySettings.set("link",t),a.model.set(_.extend(a.mapMediaToModelProps(e),{error:!1}))},i.state("image-details").on("update",t),i.state("replace-image").on("replace",t),e=wp.media.model.Attachment.prototype.sync,wp.media.model.Attachment.prototype.sync=function(){return o.Deferred().rejectWith(this).promise()},i.on("close",function(){i.detach(),wp.media.model.Attachment.prototype.sync=e}),i.open()},getEmbedResetProps:function(){return _.extend(a.MediaWidgetControl.prototype.getEmbedResetProps.call(this),{size:"full",width:0,height:0})},getModelPropsFromMediaFrame:function(e){return _.omit(a.MediaWidgetControl.prototype.getModelPropsFromMediaFrame.call(this,e),"image_title")},mapModelToPreviewTemplateProps:function(){var e=this,t=e.model.get("url"),i=a.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call(e);return i.currentFilename=t?t.replace(/\?.*$/,"").replace(/^.+\//,""):"",i.link_url=e.model.get("link_url"),i}});a.controlConstructors.media_image=t,a.modelConstructors.media_image=e}(wp.mediaWidgets,jQuery);
\ No newline at end of file diff --git a/wp-admin/js/widgets/media-video-widget.js b/wp-admin/js/widgets/media-video-widget.js new file mode 100644 index 0000000..56a8ff1 --- /dev/null +++ b/wp-admin/js/widgets/media-video-widget.js @@ -0,0 +1,256 @@ +/** + * @output wp-admin/js/widgets/media-video-widget.js + */ + +/* eslint consistent-this: [ "error", "control" ] */ +(function( component ) { + 'use strict'; + + var VideoWidgetModel, VideoWidgetControl, VideoDetailsMediaFrame; + + /** + * Custom video details frame that removes the replace-video state. + * + * @class wp.mediaWidgets.controlConstructors~VideoDetailsMediaFrame + * @augments wp.media.view.MediaFrame.VideoDetails + * + * @private + */ + VideoDetailsMediaFrame = wp.media.view.MediaFrame.VideoDetails.extend(/** @lends wp.mediaWidgets.controlConstructors~VideoDetailsMediaFrame.prototype */{ + + /** + * Create the default states. + * + * @return {void} + */ + createStates: function createStates() { + this.states.add([ + new wp.media.controller.VideoDetails({ + media: this.media + }), + + new wp.media.controller.MediaLibrary({ + type: 'video', + id: 'add-video-source', + title: wp.media.view.l10n.videoAddSourceTitle, + toolbar: 'add-video-source', + media: this.media, + menu: false + }), + + new wp.media.controller.MediaLibrary({ + type: 'text', + id: 'add-track', + title: wp.media.view.l10n.videoAddTrackTitle, + toolbar: 'add-track', + media: this.media, + menu: 'video-details' + }) + ]); + } + }); + + /** + * Video widget model. + * + * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @class wp.mediaWidgets.modelConstructors.media_video + * @augments wp.mediaWidgets.MediaWidgetModel + */ + VideoWidgetModel = component.MediaWidgetModel.extend({}); + + /** + * Video widget control. + * + * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports. + * + * @class wp.mediaWidgets.controlConstructors.media_video + * @augments wp.mediaWidgets.MediaWidgetControl + */ + VideoWidgetControl = component.MediaWidgetControl.extend(/** @lends wp.mediaWidgets.controlConstructors.media_video.prototype */{ + + /** + * Show display settings. + * + * @type {boolean} + */ + showDisplaySettings: false, + + /** + * Cache of oembed responses. + * + * @type {Object} + */ + oembedResponses: {}, + + /** + * Map model props to media frame props. + * + * @param {Object} modelProps - Model props. + * @return {Object} Media frame props. + */ + mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) { + var control = this, mediaFrameProps; + mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps ); + mediaFrameProps.link = 'embed'; + return mediaFrameProps; + }, + + /** + * Fetches embed data for external videos. + * + * @return {void} + */ + fetchEmbed: function fetchEmbed() { + var control = this, url; + url = control.model.get( 'url' ); + + // If we already have a local cache of the embed response, return. + if ( control.oembedResponses[ url ] ) { + return; + } + + // If there is an in-flight embed request, abort it. + if ( control.fetchEmbedDfd && 'pending' === control.fetchEmbedDfd.state() ) { + control.fetchEmbedDfd.abort(); + } + + control.fetchEmbedDfd = wp.apiRequest({ + url: wp.media.view.settings.oEmbedProxyUrl, + data: { + url: control.model.get( 'url' ), + maxwidth: control.model.get( 'width' ), + maxheight: control.model.get( 'height' ), + discover: false + }, + type: 'GET', + dataType: 'json', + context: control + }); + + control.fetchEmbedDfd.done( function( response ) { + control.oembedResponses[ url ] = response; + control.renderPreview(); + }); + + control.fetchEmbedDfd.fail( function() { + control.oembedResponses[ url ] = null; + }); + }, + + /** + * Whether a url is a supported external host. + * + * @deprecated since 4.9. + * + * @return {boolean} Whether url is a supported video host. + */ + isHostedVideo: function isHostedVideo() { + return true; + }, + + /** + * Render preview. + * + * @return {void} + */ + renderPreview: function renderPreview() { + var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl, poster, html = '', isOEmbed = false, mime, error, urlParser, matches; + attachmentId = control.model.get( 'attachment_id' ); + attachmentUrl = control.model.get( 'url' ); + error = control.model.get( 'error' ); + + if ( ! attachmentId && ! attachmentUrl ) { + return; + } + + // Verify the selected attachment mime is supported. + mime = control.selectedAttachment.get( 'mime' ); + if ( mime && attachmentId ) { + if ( ! _.contains( _.values( wp.media.view.settings.embedMimes ), mime ) ) { + error = 'unsupported_file_type'; + } + } else if ( ! attachmentId ) { + urlParser = document.createElement( 'a' ); + urlParser.href = attachmentUrl; + matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ ); + if ( matches ) { + if ( ! _.contains( _.keys( wp.media.view.settings.embedMimes ), matches[1] ) ) { + error = 'unsupported_file_type'; + } + } else { + isOEmbed = true; + } + } + + if ( isOEmbed ) { + control.fetchEmbed(); + if ( control.oembedResponses[ attachmentUrl ] ) { + poster = control.oembedResponses[ attachmentUrl ].thumbnail_url; + html = control.oembedResponses[ attachmentUrl ].html.replace( /\swidth="\d+"/, ' width="100%"' ).replace( /\sheight="\d+"/, '' ); + } + } + + previewContainer = control.$el.find( '.media-widget-preview' ); + previewTemplate = wp.template( 'wp-media-widget-video-preview' ); + + previewContainer.html( previewTemplate({ + model: { + attachment_id: attachmentId, + html: html, + src: attachmentUrl, + poster: poster + }, + is_oembed: isOEmbed, + error: error + })); + wp.mediaelement.initialize(); + }, + + /** + * Open the media image-edit frame to modify the selected item. + * + * @return {void} + */ + editMedia: function editMedia() { + var control = this, mediaFrame, metadata, updateCallback; + + metadata = control.mapModelToMediaFrameProps( control.model.toJSON() ); + + // Set up the media frame. + mediaFrame = new VideoDetailsMediaFrame({ + frame: 'video', + state: 'video-details', + metadata: metadata + }); + wp.media.frame = mediaFrame; + mediaFrame.$el.addClass( 'media-widget' ); + + updateCallback = function( mediaFrameProps ) { + + // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. + control.selectedAttachment.set( mediaFrameProps ); + + control.model.set( _.extend( + _.omit( control.model.defaults(), 'title' ), + control.mapMediaToModelProps( mediaFrameProps ), + { error: false } + ) ); + }; + + mediaFrame.state( 'video-details' ).on( 'update', updateCallback ); + mediaFrame.state( 'replace-video' ).on( 'replace', updateCallback ); + mediaFrame.on( 'close', function() { + mediaFrame.detach(); + }); + + mediaFrame.open(); + } + }); + + // Exports. + component.controlConstructors.media_video = VideoWidgetControl; + component.modelConstructors.media_video = VideoWidgetModel; + +})( wp.mediaWidgets ); diff --git a/wp-admin/js/widgets/media-video-widget.min.js b/wp-admin/js/widgets/media-video-widget.min.js new file mode 100644 index 0000000..653d52c --- /dev/null +++ b/wp-admin/js/widgets/media-video-widget.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(t){"use strict";var i=wp.media.view.MediaFrame.VideoDetails.extend({createStates:function(){this.states.add([new wp.media.controller.VideoDetails({media:this.media}),new wp.media.controller.MediaLibrary({type:"video",id:"add-video-source",title:wp.media.view.l10n.videoAddSourceTitle,toolbar:"add-video-source",media:this.media,menu:!1}),new wp.media.controller.MediaLibrary({type:"text",id:"add-track",title:wp.media.view.l10n.videoAddTrackTitle,toolbar:"add-track",media:this.media,menu:"video-details"})])}}),e=t.MediaWidgetModel.extend({}),d=t.MediaWidgetControl.extend({showDisplaySettings:!1,oembedResponses:{},mapModelToMediaFrameProps:function(e){e=t.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call(this,e);return e.link="embed",e},fetchEmbed:function(){var t=this,d=t.model.get("url");t.oembedResponses[d]||(t.fetchEmbedDfd&&"pending"===t.fetchEmbedDfd.state()&&t.fetchEmbedDfd.abort(),t.fetchEmbedDfd=wp.apiRequest({url:wp.media.view.settings.oEmbedProxyUrl,data:{url:t.model.get("url"),maxwidth:t.model.get("width"),maxheight:t.model.get("height"),discover:!1},type:"GET",dataType:"json",context:t}),t.fetchEmbedDfd.done(function(e){t.oembedResponses[d]=e,t.renderPreview()}),t.fetchEmbedDfd.fail(function(){t.oembedResponses[d]=null}))},isHostedVideo:function(){return!0},renderPreview:function(){var e,t,d=this,i="",o=!1,a=d.model.get("attachment_id"),s=d.model.get("url"),m=d.model.get("error");(a||s)&&((t=d.selectedAttachment.get("mime"))&&a?_.contains(_.values(wp.media.view.settings.embedMimes),t)||(m="unsupported_file_type"):a||((t=document.createElement("a")).href=s,(t=t.pathname.toLowerCase().match(/\.(\w+)$/))?_.contains(_.keys(wp.media.view.settings.embedMimes),t[1])||(m="unsupported_file_type"):o=!0),o&&(d.fetchEmbed(),d.oembedResponses[s])&&(e=d.oembedResponses[s].thumbnail_url,i=d.oembedResponses[s].html.replace(/\swidth="\d+"/,' width="100%"').replace(/\sheight="\d+"/,"")),t=d.$el.find(".media-widget-preview"),d=wp.template("wp-media-widget-video-preview"),t.html(d({model:{attachment_id:a,html:i,src:s,poster:e},is_oembed:o,error:m})),wp.mediaelement.initialize())},editMedia:function(){var t=this,e=t.mapModelToMediaFrameProps(t.model.toJSON()),d=new i({frame:"video",state:"video-details",metadata:e});(wp.media.frame=d).$el.addClass("media-widget"),e=function(e){t.selectedAttachment.set(e),t.model.set(_.extend(_.omit(t.model.defaults(),"title"),t.mapMediaToModelProps(e),{error:!1}))},d.state("video-details").on("update",e),d.state("replace-video").on("replace",e),d.on("close",function(){d.detach()}),d.open()}});t.controlConstructors.media_video=d,t.modelConstructors.media_video=e}(wp.mediaWidgets);
\ No newline at end of file diff --git a/wp-admin/js/widgets/media-widgets.js b/wp-admin/js/widgets/media-widgets.js new file mode 100644 index 0000000..2ee00a8 --- /dev/null +++ b/wp-admin/js/widgets/media-widgets.js @@ -0,0 +1,1336 @@ +/** + * @output wp-admin/js/widgets/media-widgets.js + */ + +/* eslint consistent-this: [ "error", "control" ] */ + +/** + * @namespace wp.mediaWidgets + * @memberOf wp + */ +wp.mediaWidgets = ( function( $ ) { + 'use strict'; + + var component = {}; + + /** + * Widget control (view) constructors, mapping widget id_base to subclass of MediaWidgetControl. + * + * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base. + * + * @memberOf wp.mediaWidgets + * + * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>} + */ + component.controlConstructors = {}; + + /** + * Widget model constructors, mapping widget id_base to subclass of MediaWidgetModel. + * + * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base. + * + * @memberOf wp.mediaWidgets + * + * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>} + */ + component.modelConstructors = {}; + + component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend(/** @lends wp.mediaWidgets.PersistentDisplaySettingsLibrary.prototype */{ + + /** + * Library which persists the customized display settings across selections. + * + * @constructs wp.mediaWidgets.PersistentDisplaySettingsLibrary + * @augments wp.media.controller.Library + * + * @param {Object} options - Options. + * + * @return {void} + */ + initialize: function initialize( options ) { + _.bindAll( this, 'handleDisplaySettingChange' ); + wp.media.controller.Library.prototype.initialize.call( this, options ); + }, + + /** + * Sync changes to the current display settings back into the current customized. + * + * @param {Backbone.Model} displaySettings - Modified display settings. + * @return {void} + */ + handleDisplaySettingChange: function handleDisplaySettingChange( displaySettings ) { + this.get( 'selectedDisplaySettings' ).set( displaySettings.attributes ); + }, + + /** + * Get the display settings model. + * + * Model returned is updated with the current customized display settings, + * and an event listener is added so that changes made to the settings + * will sync back into the model storing the session's customized display + * settings. + * + * @param {Backbone.Model} model - Display settings model. + * @return {Backbone.Model} Display settings model. + */ + display: function getDisplaySettingsModel( model ) { + var display, selectedDisplaySettings = this.get( 'selectedDisplaySettings' ); + display = wp.media.controller.Library.prototype.display.call( this, model ); + + display.off( 'change', this.handleDisplaySettingChange ); // Prevent duplicated event handlers. + display.set( selectedDisplaySettings.attributes ); + if ( 'custom' === selectedDisplaySettings.get( 'link_type' ) ) { + display.linkUrl = selectedDisplaySettings.get( 'link_url' ); + } + display.on( 'change', this.handleDisplaySettingChange ); + return display; + } + }); + + /** + * Extended view for managing the embed UI. + * + * @class wp.mediaWidgets.MediaEmbedView + * @augments wp.media.view.Embed + */ + component.MediaEmbedView = wp.media.view.Embed.extend(/** @lends wp.mediaWidgets.MediaEmbedView.prototype */{ + + /** + * Initialize. + * + * @since 4.9.0 + * + * @param {Object} options - Options. + * @return {void} + */ + initialize: function( options ) { + var view = this, embedController; // eslint-disable-line consistent-this + wp.media.view.Embed.prototype.initialize.call( view, options ); + if ( 'image' !== view.controller.options.mimeType ) { + embedController = view.controller.states.get( 'embed' ); + embedController.off( 'scan', embedController.scanImage, embedController ); + } + }, + + /** + * Refresh embed view. + * + * Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field. + * + * @return {void} + */ + refresh: function refresh() { + /** + * @class wp.mediaWidgets~Constructor + */ + var Constructor; + + if ( 'image' === this.controller.options.mimeType ) { + Constructor = wp.media.view.EmbedImage; + } else { + + // This should be eliminated once #40450 lands of when this is merged into core. + Constructor = wp.media.view.EmbedLink.extend(/** @lends wp.mediaWidgets~Constructor.prototype */{ + + /** + * Set the disabled state on the Add to Widget button. + * + * @param {boolean} disabled - Disabled. + * @return {void} + */ + setAddToWidgetButtonDisabled: function setAddToWidgetButtonDisabled( disabled ) { + this.views.parent.views.parent.views.get( '.media-frame-toolbar' )[0].$el.find( '.media-button-select' ).prop( 'disabled', disabled ); + }, + + /** + * Set or clear an error notice. + * + * @param {string} notice - Notice. + * @return {void} + */ + setErrorNotice: function setErrorNotice( notice ) { + var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this + + noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' ); + if ( ! notice ) { + if ( noticeContainer.length ) { + noticeContainer.slideUp( 'fast' ); + } + } else { + if ( ! noticeContainer.length ) { + noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt"></div>' ); + noticeContainer.hide(); + embedLinkView.views.parent.$el.prepend( noticeContainer ); + } + noticeContainer.empty(); + noticeContainer.append( $( '<p>', { + html: notice + })); + noticeContainer.slideDown( 'fast' ); + } + }, + + /** + * Update oEmbed. + * + * @since 4.9.0 + * + * @return {void} + */ + updateoEmbed: function() { + var embedLinkView = this, url; // eslint-disable-line consistent-this + + url = embedLinkView.model.get( 'url' ); + + // Abort if the URL field was emptied out. + if ( ! url ) { + embedLinkView.setErrorNotice( '' ); + embedLinkView.setAddToWidgetButtonDisabled( true ); + return; + } + + if ( ! url.match( /^(http|https):\/\/.+\// ) ) { + embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' ); + embedLinkView.setAddToWidgetButtonDisabled( true ); + } + + wp.media.view.EmbedLink.prototype.updateoEmbed.call( embedLinkView ); + }, + + /** + * Fetch media. + * + * @return {void} + */ + fetch: function() { + var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser, url, re, youTubeEmbedMatch; // eslint-disable-line consistent-this + url = embedLinkView.model.get( 'url' ); + + if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) { + embedLinkView.dfd.abort(); + } + + fetchSuccess = function( response ) { + embedLinkView.renderoEmbed({ + data: { + body: response + } + }); + + embedLinkView.controller.$el.find( '#embed-url-field' ).removeClass( 'invalid' ); + embedLinkView.setErrorNotice( '' ); + embedLinkView.setAddToWidgetButtonDisabled( false ); + }; + + urlParser = document.createElement( 'a' ); + urlParser.href = url; + matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ ); + if ( matches ) { + fileExt = matches[1]; + if ( ! wp.media.view.settings.embedMimes[ fileExt ] ) { + embedLinkView.renderFail(); + } else if ( 0 !== wp.media.view.settings.embedMimes[ fileExt ].indexOf( embedLinkView.controller.options.mimeType ) ) { + embedLinkView.renderFail(); + } else { + fetchSuccess( '<!--success-->' ); + } + return; + } + + // Support YouTube embed links. + re = /https?:\/\/www\.youtube\.com\/embed\/([^/]+)/; + youTubeEmbedMatch = re.exec( url ); + if ( youTubeEmbedMatch ) { + url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ]; + // silently change url to proper oembed-able version. + embedLinkView.model.attributes.url = url; + } + + embedLinkView.dfd = wp.apiRequest({ + url: wp.media.view.settings.oEmbedProxyUrl, + data: { + url: url, + maxwidth: embedLinkView.model.get( 'width' ), + maxheight: embedLinkView.model.get( 'height' ), + discover: false + }, + type: 'GET', + dataType: 'json', + context: embedLinkView + }); + + embedLinkView.dfd.done( function( response ) { + if ( embedLinkView.controller.options.mimeType !== response.type ) { + embedLinkView.renderFail(); + return; + } + fetchSuccess( response.html ); + }); + embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) ); + }, + + /** + * Handle render failure. + * + * Overrides the {EmbedLink#renderFail()} method to prevent showing the "Link Text" field. + * The element is getting display:none in the stylesheet, but the underlying method uses + * uses {jQuery.fn.show()} which adds an inline style. This avoids the need for !important. + * + * @return {void} + */ + renderFail: function renderFail() { + var embedLinkView = this; // eslint-disable-line consistent-this + embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' ); + embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' ); + embedLinkView.setAddToWidgetButtonDisabled( true ); + } + }); + } + + this.settings( new Constructor({ + controller: this.controller, + model: this.model.props, + priority: 40 + })); + } + }); + + /** + * Custom media frame for selecting uploaded media or providing media by URL. + * + * @class wp.mediaWidgets.MediaFrameSelect + * @augments wp.media.view.MediaFrame.Post + */ + component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend(/** @lends wp.mediaWidgets.MediaFrameSelect.prototype */{ + + /** + * Create the default states. + * + * @return {void} + */ + createStates: function createStates() { + var mime = this.options.mimeType, specificMimes = []; + _.each( wp.media.view.settings.embedMimes, function( embedMime ) { + if ( 0 === embedMime.indexOf( mime ) ) { + specificMimes.push( embedMime ); + } + }); + if ( specificMimes.length > 0 ) { + mime = specificMimes; + } + + this.states.add([ + + // Main states. + new component.PersistentDisplaySettingsLibrary({ + id: 'insert', + title: this.options.title, + selection: this.options.selection, + priority: 20, + toolbar: 'main-insert', + filterable: 'dates', + library: wp.media.query({ + type: mime + }), + multiple: false, + editable: true, + + selectedDisplaySettings: this.options.selectedDisplaySettings, + displaySettings: _.isUndefined( this.options.showDisplaySettings ) ? true : this.options.showDisplaySettings, + displayUserSettings: false // We use the display settings from the current/default widget instance props. + }), + + new wp.media.controller.EditImage({ model: this.options.editImage }), + + // Embed states. + new wp.media.controller.Embed({ + metadata: this.options.metadata, + type: 'image' === this.options.mimeType ? 'image' : 'link', + invalidEmbedTypeError: this.options.invalidEmbedTypeError + }) + ]); + }, + + /** + * Main insert toolbar. + * + * Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text. + * + * @param {wp.Backbone.View} view - Toolbar view. + * @this {wp.media.controller.Library} + * @return {void} + */ + mainInsertToolbar: function mainInsertToolbar( view ) { + var controller = this; // eslint-disable-line consistent-this + view.set( 'insert', { + style: 'primary', + priority: 80, + text: controller.options.text, // The whole reason for the fork. + requires: { selection: true }, + + /** + * Handle click. + * + * @ignore + * + * @fires wp.media.controller.State#insert() + * @return {void} + */ + click: function onClick() { + var state = controller.state(), + selection = state.get( 'selection' ); + + controller.close(); + state.trigger( 'insert', selection ).reset(); + } + }); + }, + + /** + * Main embed toolbar. + * + * Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text. + * + * @param {wp.Backbone.View} toolbar - Toolbar view. + * @this {wp.media.controller.Library} + * @return {void} + */ + mainEmbedToolbar: function mainEmbedToolbar( toolbar ) { + toolbar.view = new wp.media.view.Toolbar.Embed({ + controller: this, + text: this.options.text, + event: 'insert' + }); + }, + + /** + * Embed content. + * + * Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field. + * + * @return {void} + */ + embedContent: function embedContent() { + var view = new component.MediaEmbedView({ + controller: this, + model: this.state() + }).render(); + + this.content.set( view ); + } + }); + + component.MediaWidgetControl = Backbone.View.extend(/** @lends wp.mediaWidgets.MediaWidgetControl.prototype */{ + + /** + * Translation strings. + * + * The mapping of translation strings is handled by media widget subclasses, + * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). + * + * @type {Object} + */ + l10n: { + add_to_widget: '{{add_to_widget}}', + add_media: '{{add_media}}' + }, + + /** + * Widget ID base. + * + * This may be defined by the subclass. It may be exported from PHP to JS + * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not, + * it will attempt to be discovered by looking to see if this control + * instance extends each member of component.controlConstructors, and if + * it does extend one, will use the key as the id_base. + * + * @type {string} + */ + id_base: '', + + /** + * Mime type. + * + * This must be defined by the subclass. It may be exported from PHP to JS + * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). + * + * @type {string} + */ + mime_type: '', + + /** + * View events. + * + * @type {Object} + */ + events: { + 'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick', + 'click .select-media': 'selectMedia', + 'click .placeholder': 'selectMedia', + 'click .edit-media': 'editMedia' + }, + + /** + * Show display settings. + * + * @type {boolean} + */ + showDisplaySettings: true, + + /** + * Media Widget Control. + * + * @constructs wp.mediaWidgets.MediaWidgetControl + * @augments Backbone.View + * @abstract + * + * @param {Object} options - Options. + * @param {Backbone.Model} options.model - Model. + * @param {jQuery} options.el - Control field container element. + * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. + * + * @return {void} + */ + initialize: function initialize( options ) { + var control = this; + + Backbone.View.prototype.initialize.call( control, options ); + + if ( ! ( control.model instanceof component.MediaWidgetModel ) ) { + throw new Error( 'Missing options.model' ); + } + if ( ! options.el ) { + throw new Error( 'Missing options.el' ); + } + if ( ! options.syncContainer ) { + throw new Error( 'Missing options.syncContainer' ); + } + + control.syncContainer = options.syncContainer; + + control.$el.addClass( 'media-widget-control' ); + + // Allow methods to be passed in with control context preserved. + _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' ); + + if ( ! control.id_base ) { + _.find( component.controlConstructors, function( Constructor, idBase ) { + if ( control instanceof Constructor ) { + control.id_base = idBase; + return true; + } + return false; + }); + if ( ! control.id_base ) { + throw new Error( 'Missing id_base.' ); + } + } + + // Track attributes needed to renderPreview in it's own model. + control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() ); + + // Re-render the preview when the attachment changes. + control.selectedAttachment = new wp.media.model.Attachment(); + control.renderPreview = _.debounce( control.renderPreview ); + control.listenTo( control.previewTemplateProps, 'change', control.renderPreview ); + + // Make sure a copy of the selected attachment is always fetched. + control.model.on( 'change:attachment_id', control.updateSelectedAttachment ); + control.model.on( 'change:url', control.updateSelectedAttachment ); + control.updateSelectedAttachment(); + + /* + * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state. + * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model + * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>. + */ + control.listenTo( control.model, 'change', control.syncModelToInputs ); + control.listenTo( control.model, 'change', control.syncModelToPreviewProps ); + control.listenTo( control.model, 'change', control.render ); + + // Update the title. + control.$el.on( 'input change', '.title', function updateTitle() { + control.model.set({ + title: $( this ).val().trim() + }); + }); + + // Update link_url attribute. + control.$el.on( 'input change', '.link', function updateLinkUrl() { + var linkUrl = $( this ).val().trim(), linkType = 'custom'; + if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) { + linkType = 'post'; + } else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) { + linkType = 'file'; + } + control.model.set( { + link_url: linkUrl, + link_type: linkType + }); + + // Update display settings for the next time the user opens to select from the media library. + control.displaySettings.set( { + link: linkType, + linkUrl: linkUrl + }); + }); + + /* + * Copy current display settings from the widget model to serve as basis + * of customized display settings for the current media frame session. + * Changes to display settings will be synced into this model, and + * when a new selection is made, the settings from this will be synced + * into that AttachmentDisplay's model to persist the setting changes. + */ + control.displaySettings = new Backbone.Model( _.pick( + control.mapModelToMediaFrameProps( + _.extend( control.model.defaults(), control.model.toJSON() ) + ), + _.keys( wp.media.view.settings.defaultProps ) + ) ); + }, + + /** + * Update the selected attachment if necessary. + * + * @return {void} + */ + updateSelectedAttachment: function updateSelectedAttachment() { + var control = this, attachment; + + if ( 0 === control.model.get( 'attachment_id' ) ) { + control.selectedAttachment.clear(); + control.model.set( 'error', false ); + } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) { + attachment = new wp.media.model.Attachment({ + id: control.model.get( 'attachment_id' ) + }); + attachment.fetch() + .done( function done() { + control.model.set( 'error', false ); + control.selectedAttachment.set( attachment.toJSON() ); + }) + .fail( function fail() { + control.model.set( 'error', 'missing_attachment' ); + }); + } + }, + + /** + * Sync the model attributes to the hidden inputs, and update previewTemplateProps. + * + * @return {void} + */ + syncModelToPreviewProps: function syncModelToPreviewProps() { + var control = this; + control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() ); + }, + + /** + * Sync the model attributes to the hidden inputs, and update previewTemplateProps. + * + * @return {void} + */ + syncModelToInputs: function syncModelToInputs() { + var control = this; + control.syncContainer.find( '.media-widget-instance-property' ).each( function() { + var input = $( this ), value, propertyName; + propertyName = input.data( 'property' ); + value = control.model.get( propertyName ); + if ( _.isUndefined( value ) ) { + return; + } + + if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) { + value = value.join( ',' ); + } else if ( 'boolean' === control.model.schema[ propertyName ].type ) { + value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''. + } else { + value = String( value ); + } + + if ( input.val() !== value ) { + input.val( value ); + input.trigger( 'change' ); + } + }); + }, + + /** + * Get template. + * + * @return {Function} Template. + */ + template: function template() { + var control = this; + if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) { + throw new Error( 'Missing widget control template for ' + control.id_base ); + } + return wp.template( 'widget-media-' + control.id_base + '-control' ); + }, + + /** + * Render template. + * + * @return {void} + */ + render: function render() { + var control = this, titleInput; + + if ( ! control.templateRendered ) { + control.$el.html( control.template()( control.model.toJSON() ) ); + control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes. + control.templateRendered = true; + } + + titleInput = control.$el.find( '.title' ); + if ( ! titleInput.is( document.activeElement ) ) { + titleInput.val( control.model.get( 'title' ) ); + } + + control.$el.toggleClass( 'selected', control.isSelected() ); + }, + + /** + * Render media preview. + * + * @abstract + * @return {void} + */ + renderPreview: function renderPreview() { + throw new Error( 'renderPreview must be implemented' ); + }, + + /** + * Whether a media item is selected. + * + * @return {boolean} Whether selected and no error. + */ + isSelected: function isSelected() { + var control = this; + + if ( control.model.get( 'error' ) ) { + return false; + } + + return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) ); + }, + + /** + * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice. + * + * @param {jQuery.Event} event - Event. + * @return {void} + */ + handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) { + var control = this; + event.preventDefault(); + control.selectMedia(); + }, + + /** + * Open the media select frame to chose an item. + * + * @return {void} + */ + selectMedia: function selectMedia() { + var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = []; + + if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) { + selectionModels.push( control.selectedAttachment ); + } + + selection = new wp.media.model.Selection( selectionModels, { multiple: false } ); + + mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() ); + if ( mediaFrameProps.size ) { + control.displaySettings.set( 'size', mediaFrameProps.size ); + } + + mediaFrame = new component.MediaFrameSelect({ + title: control.l10n.add_media, + frame: 'post', + text: control.l10n.add_to_widget, + selection: selection, + mimeType: control.mime_type, + selectedDisplaySettings: control.displaySettings, + showDisplaySettings: control.showDisplaySettings, + metadata: mediaFrameProps, + state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert', + invalidEmbedTypeError: control.l10n.unsupported_file_type + }); + wp.media.frame = mediaFrame; // See wp.media(). + + // Handle selection of a media item. + mediaFrame.on( 'insert', function onInsert() { + var attachment = {}, state = mediaFrame.state(); + + // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. + if ( 'embed' === state.get( 'id' ) ) { + _.extend( attachment, { id: 0 }, state.props.toJSON() ); + } else { + _.extend( attachment, state.get( 'selection' ).first().toJSON() ); + } + + control.selectedAttachment.set( attachment ); + control.model.set( 'error', false ); + + // Update widget instance. + control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) ); + }); + + // Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>. + defaultSync = wp.media.model.Attachment.prototype.sync; + wp.media.model.Attachment.prototype.sync = function( method ) { + if ( 'delete' === method ) { + return defaultSync.apply( this, arguments ); + } else { + return $.Deferred().rejectWith( this ).promise(); + } + }; + mediaFrame.on( 'close', function onClose() { + wp.media.model.Attachment.prototype.sync = defaultSync; + }); + + mediaFrame.$el.addClass( 'media-widget' ); + mediaFrame.open(); + + // Clear the selected attachment when it is deleted in the media select frame. + if ( selection ) { + selection.on( 'destroy', function onDestroy( attachment ) { + if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) { + control.model.set({ + attachment_id: 0, + url: '' + }); + } + }); + } + + /* + * Make sure focus is set inside of modal so that hitting Esc will close + * the modal and not inadvertently cause the widget to collapse in the customizer. + */ + mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus(); + }, + + /** + * Get the instance props from the media selection frame. + * + * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame. + * @return {Object} Props. + */ + getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) { + var control = this, state, mediaFrameProps, modelProps; + + state = mediaFrame.state(); + if ( 'insert' === state.get( 'id' ) ) { + mediaFrameProps = state.get( 'selection' ).first().toJSON(); + mediaFrameProps.postUrl = mediaFrameProps.link; + + if ( control.showDisplaySettings ) { + _.extend( + mediaFrameProps, + mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON() + ); + } + if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) { + mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url; + } + } else if ( 'embed' === state.get( 'id' ) ) { + mediaFrameProps = _.extend( + state.props.toJSON(), + { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`. + control.model.getEmbedResetProps() + ); + } else { + throw new Error( 'Unexpected state: ' + state.get( 'id' ) ); + } + + if ( mediaFrameProps.id ) { + mediaFrameProps.attachment_id = mediaFrameProps.id; + } + + modelProps = control.mapMediaToModelProps( mediaFrameProps ); + + // Clear the extension prop so sources will be reset for video and audio media. + _.each( wp.media.view.settings.embedExts, function( ext ) { + if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) { + modelProps[ ext ] = ''; + } + }); + + return modelProps; + }, + + /** + * Map media frame props to model props. + * + * @param {Object} mediaFrameProps - Media frame props. + * @return {Object} Model props. + */ + mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) { + var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension; + _.each( control.model.schema, function( fieldSchema, modelProp ) { + + // Ignore widget title attribute. + if ( 'title' === modelProp ) { + return; + } + mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp; + }); + + _.each( mediaFrameProps, function( value, mediaProp ) { + var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp; + if ( control.model.schema[ propName ] ) { + modelProps[ propName ] = value; + } + }); + + if ( 'custom' === mediaFrameProps.size ) { + modelProps.width = mediaFrameProps.customWidth; + modelProps.height = mediaFrameProps.customHeight; + } + + if ( 'post' === mediaFrameProps.link ) { + modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl; + } else if ( 'file' === mediaFrameProps.link ) { + modelProps.link_url = mediaFrameProps.url; + } + + // Because some media frames use `id` instead of `attachment_id`. + if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) { + modelProps.attachment_id = mediaFrameProps.id; + } + + if ( mediaFrameProps.url ) { + extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase(); + if ( extension in control.model.schema ) { + modelProps[ extension ] = mediaFrameProps.url; + } + } + + // Always omit the titles derived from mediaFrameProps. + return _.omit( modelProps, 'title' ); + }, + + /** + * Map model props to media frame props. + * + * @param {Object} modelProps - Model props. + * @return {Object} Media frame props. + */ + mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) { + var control = this, mediaFrameProps = {}; + + _.each( modelProps, function( value, modelProp ) { + var fieldSchema = control.model.schema[ modelProp ] || {}; + mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value; + }); + + // Some media frames use attachment_id. + mediaFrameProps.attachment_id = mediaFrameProps.id; + + if ( 'custom' === mediaFrameProps.size ) { + mediaFrameProps.customWidth = control.model.get( 'width' ); + mediaFrameProps.customHeight = control.model.get( 'height' ); + } + + return mediaFrameProps; + }, + + /** + * Map model props to previewTemplateProps. + * + * @return {Object} Preview Template Props. + */ + mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() { + var control = this, previewTemplateProps = {}; + _.each( control.model.schema, function( value, prop ) { + if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) { + previewTemplateProps[ prop ] = control.model.get( prop ); + } + }); + + // Templates need to be aware of the error. + previewTemplateProps.error = control.model.get( 'error' ); + return previewTemplateProps; + }, + + /** + * Open the media frame to modify the selected item. + * + * @abstract + * @return {void} + */ + editMedia: function editMedia() { + throw new Error( 'editMedia not implemented' ); + } + }); + + /** + * Media widget model. + * + * @class wp.mediaWidgets.MediaWidgetModel + * @augments Backbone.Model + */ + component.MediaWidgetModel = Backbone.Model.extend(/** @lends wp.mediaWidgets.MediaWidgetModel.prototype */{ + + /** + * Id attribute. + * + * @type {string} + */ + idAttribute: 'widget_id', + + /** + * Instance schema. + * + * This adheres to JSON Schema and subclasses should have their schema + * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). + * + * @type {Object.<string, Object>} + */ + schema: { + title: { + type: 'string', + 'default': '' + }, + attachment_id: { + type: 'integer', + 'default': 0 + }, + url: { + type: 'string', + 'default': '' + } + }, + + /** + * Get default attribute values. + * + * @return {Object} Mapping of property names to their default values. + */ + defaults: function() { + var defaults = {}; + _.each( this.schema, function( fieldSchema, field ) { + defaults[ field ] = fieldSchema['default']; + }); + return defaults; + }, + + /** + * Set attribute value(s). + * + * This is a wrapped version of Backbone.Model#set() which allows us to + * cast the attribute values from the hidden inputs' string values into + * the appropriate data types (integers or booleans). + * + * @param {string|Object} key - Attribute name or attribute pairs. + * @param {mixed|Object} [val] - Attribute value or options object. + * @param {Object} [options] - Options when attribute name and value are passed separately. + * @return {wp.mediaWidgets.MediaWidgetModel} This model. + */ + set: function set( key, val, options ) { + var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this + if ( null === key ) { + return model; + } + if ( 'object' === typeof key ) { + attrs = key; + opts = val; + } else { + attrs = {}; + attrs[ key ] = val; + opts = options; + } + + castedAttrs = {}; + _.each( attrs, function( value, name ) { + var type; + if ( ! model.schema[ name ] ) { + castedAttrs[ name ] = value; + return; + } + type = model.schema[ name ].type; + if ( 'array' === type ) { + castedAttrs[ name ] = value; + if ( ! _.isArray( castedAttrs[ name ] ) ) { + castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list. + } + if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) { + castedAttrs[ name ] = _.filter( + _.map( castedAttrs[ name ], function( id ) { + return parseInt( id, 10 ); + }, + function( id ) { + return 'number' === typeof id; + } + ) ); + } + } else if ( 'integer' === type ) { + castedAttrs[ name ] = parseInt( value, 10 ); + } else if ( 'boolean' === type ) { + castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value ); + } else { + castedAttrs[ name ] = value; + } + }); + + return Backbone.Model.prototype.set.call( this, castedAttrs, opts ); + }, + + /** + * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment). + * + * @return {Object} Reset/override props. + */ + getEmbedResetProps: function getEmbedResetProps() { + return { + id: 0 + }; + } + }); + + /** + * Collection of all widget model instances. + * + * @memberOf wp.mediaWidgets + * + * @type {Backbone.Collection} + */ + component.modelCollection = new ( Backbone.Collection.extend( { + model: component.MediaWidgetModel + }) )(); + + /** + * Mapping of widget ID to instances of MediaWidgetControl subclasses. + * + * @memberOf wp.mediaWidgets + * + * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>} + */ + component.widgetControls = {}; + + /** + * Handle widget being added or initialized for the first time at the widget-added event. + * + * @memberOf wp.mediaWidgets + * + * @param {jQuery.Event} event - Event. + * @param {jQuery} widgetContainer - Widget container element. + * + * @return {void} + */ + component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { + var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone; + widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen. + idBase = widgetForm.find( '> .id_base' ).val(); + widgetId = widgetForm.find( '> .widget-id' ).val(); + + // Prevent initializing already-added widgets. + if ( component.widgetControls[ widgetId ] ) { + return; + } + + ControlConstructor = component.controlConstructors[ idBase ]; + if ( ! ControlConstructor ) { + return; + } + + ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel; + + /* + * Create a container element for the widget control (Backbone.View). + * This is inserted into the DOM immediately before the .widget-content + * element because the contents of this element are essentially "managed" + * by PHP, where each widget update cause the entire element to be emptied + * and replaced with the rendered output of WP_Widget::form() which is + * sent back in Ajax request made to save/update the widget instance. + * To prevent a "flash of replaced DOM elements and re-initialized JS + * components", the JS template is rendered outside of the normal form + * container. + */ + fieldContainer = $( '<div></div>' ); + syncContainer = widgetContainer.find( '.widget-content:first' ); + syncContainer.before( fieldContainer ); + + /* + * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state. + * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model + * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>. + */ + modelAttributes = {}; + syncContainer.find( '.media-widget-instance-property' ).each( function() { + var input = $( this ); + modelAttributes[ input.data( 'property' ) ] = input.val(); + }); + modelAttributes.widget_id = widgetId; + + widgetModel = new ModelConstructor( modelAttributes ); + + widgetControl = new ControlConstructor({ + el: fieldContainer, + syncContainer: syncContainer, + model: widgetModel + }); + + /* + * Render the widget once the widget parent's container finishes animating, + * as the widget-added event fires with a slideDown of the container. + * This ensures that the container's dimensions are fixed so that ME.js + * can initialize with the proper dimensions. + */ + renderWhenAnimationDone = function() { + if ( ! widgetContainer.hasClass( 'open' ) ) { + setTimeout( renderWhenAnimationDone, animatedCheckDelay ); + } else { + widgetControl.render(); + } + }; + renderWhenAnimationDone(); + + /* + * Note that the model and control currently won't ever get garbage-collected + * when a widget gets removed/deleted because there is no widget-removed event. + */ + component.modelCollection.add( [ widgetModel ] ); + component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl; + }; + + /** + * Setup widget in accessibility mode. + * + * @memberOf wp.mediaWidgets + * + * @return {void} + */ + component.setupAccessibleMode = function setupAccessibleMode() { + var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer; + widgetForm = $( '.editwidget > form' ); + if ( 0 === widgetForm.length ) { + return; + } + + idBase = widgetForm.find( '.id_base' ).val(); + + ControlConstructor = component.controlConstructors[ idBase ]; + if ( ! ControlConstructor ) { + return; + } + + widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val(); + + ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel; + fieldContainer = $( '<div></div>' ); + syncContainer = widgetForm.find( '> .widget-inside' ); + syncContainer.before( fieldContainer ); + + modelAttributes = {}; + syncContainer.find( '.media-widget-instance-property' ).each( function() { + var input = $( this ); + modelAttributes[ input.data( 'property' ) ] = input.val(); + }); + modelAttributes.widget_id = widgetId; + + widgetControl = new ControlConstructor({ + el: fieldContainer, + syncContainer: syncContainer, + model: new ModelConstructor( modelAttributes ) + }); + + component.modelCollection.add( [ widgetControl.model ] ); + component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl; + + widgetControl.render(); + }; + + /** + * Sync widget instance data sanitized from server back onto widget model. + * + * This gets called via the 'widget-updated' event when saving a widget from + * the widgets admin screen and also via the 'widget-synced' event when making + * a change to a widget in the customizer. + * + * @memberOf wp.mediaWidgets + * + * @param {jQuery.Event} event - Event. + * @param {jQuery} widgetContainer - Widget container element. + * + * @return {void} + */ + component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) { + var widgetForm, widgetContent, widgetId, widgetControl, attributes = {}; + widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); + widgetId = widgetForm.find( '> .widget-id' ).val(); + + widgetControl = component.widgetControls[ widgetId ]; + if ( ! widgetControl ) { + return; + } + + // Make sure the server-sanitized values get synced back into the model. + widgetContent = widgetForm.find( '> .widget-content' ); + widgetContent.find( '.media-widget-instance-property' ).each( function() { + var property = $( this ).data( 'property' ); + attributes[ property ] = $( this ).val(); + }); + + // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop. + widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs ); + widgetControl.model.set( attributes ); + widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs ); + }; + + /** + * Initialize functionality. + * + * This function exists to prevent the JS file from having to boot itself. + * When WordPress enqueues this script, it should have an inline script + * attached which calls wp.mediaWidgets.init(). + * + * @memberOf wp.mediaWidgets + * + * @return {void} + */ + component.init = function init() { + var $document = $( document ); + $document.on( 'widget-added', component.handleWidgetAdded ); + $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); + + /* + * Manually trigger widget-added events for media widgets on the admin + * screen once they are expanded. The widget-added event is not triggered + * for each pre-existing widget on the widgets admin screen like it is + * on the customizer. Likewise, the customizer only triggers widget-added + * when the widget is expanded to just-in-time construct the widget form + * when it is actually going to be displayed. So the following implements + * the same for the widgets admin screen, to invoke the widget-added + * handler when a pre-existing media widget is expanded. + */ + $( function initializeExistingWidgetContainers() { + var widgetContainers; + if ( 'widgets' !== window.pagenow ) { + return; + } + widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' ); + widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() { + var widgetContainer = $( this ); + component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); + }); + + // Accessibility mode. + if ( document.readyState === 'complete' ) { + // Page is fully loaded. + component.setupAccessibleMode(); + } else { + // Page is still loading. + $( window ).on( 'load', function() { + component.setupAccessibleMode(); + }); + } + }); + }; + + return component; +})( jQuery ); diff --git a/wp-admin/js/widgets/media-widgets.min.js b/wp-admin/js/widgets/media-widgets.min.js new file mode 100644 index 0000000..1e3b45d --- /dev/null +++ b/wp-admin/js/widgets/media-widgets.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +wp.mediaWidgets=function(c){"use strict";var m={controlConstructors:{},modelConstructors:{}};return m.PersistentDisplaySettingsLibrary=wp.media.controller.Library.extend({initialize:function(e){_.bindAll(this,"handleDisplaySettingChange"),wp.media.controller.Library.prototype.initialize.call(this,e)},handleDisplaySettingChange:function(e){this.get("selectedDisplaySettings").set(e.attributes)},display:function(e){var t=this.get("selectedDisplaySettings"),e=wp.media.controller.Library.prototype.display.call(this,e);return e.off("change",this.handleDisplaySettingChange),e.set(t.attributes),"custom"===t.get("link_type")&&(e.linkUrl=t.get("link_url")),e.on("change",this.handleDisplaySettingChange),e}}),m.MediaEmbedView=wp.media.view.Embed.extend({initialize:function(e){var t=this;wp.media.view.Embed.prototype.initialize.call(t,e),"image"!==t.controller.options.mimeType&&(e=t.controller.states.get("embed")).off("scan",e.scanImage,e)},refresh:function(){var e="image"===this.controller.options.mimeType?wp.media.view.EmbedImage:wp.media.view.EmbedLink.extend({setAddToWidgetButtonDisabled:function(e){this.views.parent.views.parent.views.get(".media-frame-toolbar")[0].$el.find(".media-button-select").prop("disabled",e)},setErrorNotice:function(e){var t=this.views.parent.$el.find("> .notice:first-child");e?(t.length||((t=c('<div class="media-widget-embed-notice notice notice-error notice-alt"></div>')).hide(),this.views.parent.$el.prepend(t)),t.empty(),t.append(c("<p>",{html:e})),t.slideDown("fast")):t.length&&t.slideUp("fast")},updateoEmbed:function(){var e=this,t=e.model.get("url");t?(t.match(/^(http|https):\/\/.+\//)||(e.controller.$el.find("#embed-url-field").addClass("invalid"),e.setAddToWidgetButtonDisabled(!0)),wp.media.view.EmbedLink.prototype.updateoEmbed.call(e)):(e.setErrorNotice(""),e.setAddToWidgetButtonDisabled(!0))},fetch:function(){var t,e,i=this,n=i.model.get("url");i.dfd&&"pending"===i.dfd.state()&&i.dfd.abort(),t=function(e){i.renderoEmbed({data:{body:e}}),i.controller.$el.find("#embed-url-field").removeClass("invalid"),i.setErrorNotice(""),i.setAddToWidgetButtonDisabled(!1)},(e=document.createElement("a")).href=n,(e=e.pathname.toLowerCase().match(/\.(\w+)$/))?(e=e[1],!wp.media.view.settings.embedMimes[e]||0!==wp.media.view.settings.embedMimes[e].indexOf(i.controller.options.mimeType)?i.renderFail():t("\x3c!--success--\x3e")):((e=/https?:\/\/www\.youtube\.com\/embed\/([^/]+)/.exec(n))&&(n="https://www.youtube.com/watch?v="+e[1],i.model.attributes.url=n),i.dfd=wp.apiRequest({url:wp.media.view.settings.oEmbedProxyUrl,data:{url:n,maxwidth:i.model.get("width"),maxheight:i.model.get("height"),discover:!1},type:"GET",dataType:"json",context:i}),i.dfd.done(function(e){i.controller.options.mimeType!==e.type?i.renderFail():t(e.html)}),i.dfd.fail(_.bind(i.renderFail,i)))},renderFail:function(){var e=this;e.controller.$el.find("#embed-url-field").addClass("invalid"),e.setErrorNotice(e.controller.options.invalidEmbedTypeError||"ERROR"),e.setAddToWidgetButtonDisabled(!0)}});this.settings(new e({controller:this.controller,model:this.model.props,priority:40}))}}),m.MediaFrameSelect=wp.media.view.MediaFrame.Post.extend({createStates:function(){var t=this.options.mimeType,i=[];_.each(wp.media.view.settings.embedMimes,function(e){0===e.indexOf(t)&&i.push(e)}),0<i.length&&(t=i),this.states.add([new m.PersistentDisplaySettingsLibrary({id:"insert",title:this.options.title,selection:this.options.selection,priority:20,toolbar:"main-insert",filterable:"dates",library:wp.media.query({type:t}),multiple:!1,editable:!0,selectedDisplaySettings:this.options.selectedDisplaySettings,displaySettings:!!_.isUndefined(this.options.showDisplaySettings)||this.options.showDisplaySettings,displayUserSettings:!1}),new wp.media.controller.EditImage({model:this.options.editImage}),new wp.media.controller.Embed({metadata:this.options.metadata,type:"image"===this.options.mimeType?"image":"link",invalidEmbedTypeError:this.options.invalidEmbedTypeError})])},mainInsertToolbar:function(e){var i=this;e.set("insert",{style:"primary",priority:80,text:i.options.text,requires:{selection:!0},click:function(){var e=i.state(),t=e.get("selection");i.close(),e.trigger("insert",t).reset()}})},mainEmbedToolbar:function(e){e.view=new wp.media.view.Toolbar.Embed({controller:this,text:this.options.text,event:"insert"})},embedContent:function(){var e=new m.MediaEmbedView({controller:this,model:this.state()}).render();this.content.set(e)}}),m.MediaWidgetControl=Backbone.View.extend({l10n:{add_to_widget:"{{add_to_widget}}",add_media:"{{add_media}}"},id_base:"",mime_type:"",events:{"click .notice-missing-attachment a":"handleMediaLibraryLinkClick","click .select-media":"selectMedia","click .placeholder":"selectMedia","click .edit-media":"editMedia"},showDisplaySettings:!0,initialize:function(e){var i=this;if(Backbone.View.prototype.initialize.call(i,e),!(i.model instanceof m.MediaWidgetModel))throw new Error("Missing options.model");if(!e.el)throw new Error("Missing options.el");if(!e.syncContainer)throw new Error("Missing options.syncContainer");if(i.syncContainer=e.syncContainer,i.$el.addClass("media-widget-control"),_.bindAll(i,"syncModelToInputs","render","updateSelectedAttachment","renderPreview"),!i.id_base&&(_.find(m.controlConstructors,function(e,t){return i instanceof e&&(i.id_base=t,!0)}),!i.id_base))throw new Error("Missing id_base.");i.previewTemplateProps=new Backbone.Model(i.mapModelToPreviewTemplateProps()),i.selectedAttachment=new wp.media.model.Attachment,i.renderPreview=_.debounce(i.renderPreview),i.listenTo(i.previewTemplateProps,"change",i.renderPreview),i.model.on("change:attachment_id",i.updateSelectedAttachment),i.model.on("change:url",i.updateSelectedAttachment),i.updateSelectedAttachment(),i.listenTo(i.model,"change",i.syncModelToInputs),i.listenTo(i.model,"change",i.syncModelToPreviewProps),i.listenTo(i.model,"change",i.render),i.$el.on("input change",".title",function(){i.model.set({title:c(this).val().trim()})}),i.$el.on("input change",".link",function(){var e=c(this).val().trim(),t="custom";i.selectedAttachment.get("linkUrl")===e||i.selectedAttachment.get("link")===e?t="post":i.selectedAttachment.get("url")===e&&(t="file"),i.model.set({link_url:e,link_type:t}),i.displaySettings.set({link:t,linkUrl:e})}),i.displaySettings=new Backbone.Model(_.pick(i.mapModelToMediaFrameProps(_.extend(i.model.defaults(),i.model.toJSON())),_.keys(wp.media.view.settings.defaultProps)))},updateSelectedAttachment:function(){var e,t=this;0===t.model.get("attachment_id")?(t.selectedAttachment.clear(),t.model.set("error",!1)):t.model.get("attachment_id")!==t.selectedAttachment.get("id")&&(e=new wp.media.model.Attachment({id:t.model.get("attachment_id")})).fetch().done(function(){t.model.set("error",!1),t.selectedAttachment.set(e.toJSON())}).fail(function(){t.model.set("error","missing_attachment")})},syncModelToPreviewProps:function(){this.previewTemplateProps.set(this.mapModelToPreviewTemplateProps())},syncModelToInputs:function(){var n=this;n.syncContainer.find(".media-widget-instance-property").each(function(){var e=c(this),t=e.data("property"),i=n.model.get(t);_.isUndefined(i)||(i="array"===n.model.schema[t].type&&_.isArray(i)?i.join(","):"boolean"===n.model.schema[t].type?i?"1":"":String(i),e.val()!==i&&(e.val(i),e.trigger("change")))})},template:function(){if(c("#tmpl-widget-media-"+this.id_base+"-control").length)return wp.template("widget-media-"+this.id_base+"-control");throw new Error("Missing widget control template for "+this.id_base)},render:function(){var e,t=this;t.templateRendered||(t.$el.html(t.template()(t.model.toJSON())),t.renderPreview(),t.templateRendered=!0),(e=t.$el.find(".title")).is(document.activeElement)||e.val(t.model.get("title")),t.$el.toggleClass("selected",t.isSelected())},renderPreview:function(){throw new Error("renderPreview must be implemented")},isSelected:function(){return!this.model.get("error")&&Boolean(this.model.get("attachment_id")||this.model.get("url"))},handleMediaLibraryLinkClick:function(e){e.preventDefault(),this.selectMedia()},selectMedia:function(){var i,t,e,n=this,d=[];n.isSelected()&&0!==n.model.get("attachment_id")&&d.push(n.selectedAttachment),d=new wp.media.model.Selection(d,{multiple:!1}),(e=n.mapModelToMediaFrameProps(n.model.toJSON())).size&&n.displaySettings.set("size",e.size),i=new m.MediaFrameSelect({title:n.l10n.add_media,frame:"post",text:n.l10n.add_to_widget,selection:d,mimeType:n.mime_type,selectedDisplaySettings:n.displaySettings,showDisplaySettings:n.showDisplaySettings,metadata:e,state:n.isSelected()&&0===n.model.get("attachment_id")?"embed":"insert",invalidEmbedTypeError:n.l10n.unsupported_file_type}),(wp.media.frame=i).on("insert",function(){var e={},t=i.state();"embed"===t.get("id")?_.extend(e,{id:0},t.props.toJSON()):_.extend(e,t.get("selection").first().toJSON()),n.selectedAttachment.set(e),n.model.set("error",!1),n.model.set(n.getModelPropsFromMediaFrame(i))}),t=wp.media.model.Attachment.prototype.sync,wp.media.model.Attachment.prototype.sync=function(e){return"delete"===e?t.apply(this,arguments):c.Deferred().rejectWith(this).promise()},i.on("close",function(){wp.media.model.Attachment.prototype.sync=t}),i.$el.addClass("media-widget"),i.open(),d&&d.on("destroy",function(e){n.model.get("attachment_id")===e.get("id")&&n.model.set({attachment_id:0,url:""})}),i.$el.find(".media-frame-menu .media-menu-item.active").focus()},getModelPropsFromMediaFrame:function(e){var t,i,n=this,d=e.state();if("insert"===d.get("id"))(t=d.get("selection").first().toJSON()).postUrl=t.link,n.showDisplaySettings&&_.extend(t,e.content.get(".attachments-browser").sidebar.get("display").model.toJSON()),t.sizes&&t.size&&t.sizes[t.size]&&(t.url=t.sizes[t.size].url);else{if("embed"!==d.get("id"))throw new Error("Unexpected state: "+d.get("id"));t=_.extend(d.props.toJSON(),{attachment_id:0},n.model.getEmbedResetProps())}return t.id&&(t.attachment_id=t.id),i=n.mapMediaToModelProps(t),_.each(wp.media.view.settings.embedExts,function(e){e in n.model.schema&&i.url!==i[e]&&(i[e]="")}),i},mapMediaToModelProps:function(e){var t,i=this,n={},d={};return _.each(i.model.schema,function(e,t){"title"!==t&&(n[e.media_prop||t]=t)}),_.each(e,function(e,t){t=n[t]||t;i.model.schema[t]&&(d[t]=e)}),"custom"===e.size&&(d.width=e.customWidth,d.height=e.customHeight),"post"===e.link?d.link_url=e.postUrl||e.linkUrl:"file"===e.link&&(d.link_url=e.url),!e.attachment_id&&e.id&&(d.attachment_id=e.id),e.url&&(t=e.url.replace(/#.*$/,"").replace(/\?.*$/,"").split(".").pop().toLowerCase())in i.model.schema&&(d[t]=e.url),_.omit(d,"title")},mapModelToMediaFrameProps:function(e){var n=this,d={};return _.each(e,function(e,t){var i=n.model.schema[t]||{};d[i.media_prop||t]=e}),d.attachment_id=d.id,"custom"===d.size&&(d.customWidth=n.model.get("width"),d.customHeight=n.model.get("height")),d},mapModelToPreviewTemplateProps:function(){var i=this,n={};return _.each(i.model.schema,function(e,t){e.hasOwnProperty("should_preview_update")&&!e.should_preview_update||(n[t]=i.model.get(t))}),n.error=i.model.get("error"),n},editMedia:function(){throw new Error("editMedia not implemented")}}),m.MediaWidgetModel=Backbone.Model.extend({idAttribute:"widget_id",schema:{title:{type:"string",default:""},attachment_id:{type:"integer",default:0},url:{type:"string",default:""}},defaults:function(){var i={};return _.each(this.schema,function(e,t){i[t]=e.default}),i},set:function(e,t,i){var n,d,o=this;return null===e?o:(e="object"==typeof e?(n=e,t):((n={})[e]=t,i),d={},_.each(n,function(e,t){var i;o.schema[t]?"array"===(i=o.schema[t].type)?(d[t]=e,_.isArray(d[t])||(d[t]=d[t].split(/,/)),o.schema[t].items&&"integer"===o.schema[t].items.type&&(d[t]=_.filter(_.map(d[t],function(e){return parseInt(e,10)},function(e){return"number"==typeof e})))):d[t]="integer"===i?parseInt(e,10):"boolean"===i?!(!e||"0"===e||"false"===e):e:d[t]=e}),Backbone.Model.prototype.set.call(this,d,e))},getEmbedResetProps:function(){return{id:0}}}),m.modelCollection=new(Backbone.Collection.extend({model:m.MediaWidgetModel})),m.widgetControls={},m.handleWidgetAdded=function(e,t){var i,n,d,o,a,s,r=t.find("> .widget-inside > .form, > .widget-inside > form"),l=r.find("> .id_base").val(),r=r.find("> .widget-id").val();m.widgetControls[r]||(d=m.controlConstructors[l])&&(l=m.modelConstructors[l]||m.MediaWidgetModel,i=c("<div></div>"),(n=t.find(".widget-content:first")).before(i),o={},n.find(".media-widget-instance-property").each(function(){var e=c(this);o[e.data("property")]=e.val()}),o.widget_id=r,r=new l(o),a=new d({el:i,syncContainer:n,model:r}),(s=function(){t.hasClass("open")?a.render():setTimeout(s,50)})(),m.modelCollection.add([r]),m.widgetControls[r.get("widget_id")]=a)},m.setupAccessibleMode=function(){var e,t,i,n,d,o=c(".editwidget > form");0!==o.length&&(i=o.find(".id_base").val(),t=m.controlConstructors[i])&&(e=o.find("> .widget-control-actions > .widget-id").val(),i=m.modelConstructors[i]||m.MediaWidgetModel,d=c("<div></div>"),(o=o.find("> .widget-inside")).before(d),n={},o.find(".media-widget-instance-property").each(function(){var e=c(this);n[e.data("property")]=e.val()}),n.widget_id=e,e=new t({el:d,syncContainer:o,model:new i(n)}),m.modelCollection.add([e.model]),(m.widgetControls[e.model.get("widget_id")]=e).render())},m.handleWidgetUpdated=function(e,t){var i={},t=t.find("> .widget-inside > .form, > .widget-inside > form"),n=t.find("> .widget-id").val(),n=m.widgetControls[n];n&&(t.find("> .widget-content").find(".media-widget-instance-property").each(function(){var e=c(this).data("property");i[e]=c(this).val()}),n.stopListening(n.model,"change",n.syncModelToInputs),n.model.set(i),n.listenTo(n.model,"change",n.syncModelToInputs))},m.init=function(){var e=c(document);e.on("widget-added",m.handleWidgetAdded),e.on("widget-synced widget-updated",m.handleWidgetUpdated),c(function(){"widgets"===window.pagenow&&(c(".widgets-holder-wrap:not(#available-widgets)").find("div.widget").one("click.toggle-widget-expanded",function(){var e=c(this);m.handleWidgetAdded(new jQuery.Event("widget-added"),e)}),"complete"===document.readyState?m.setupAccessibleMode():c(window).on("load",function(){m.setupAccessibleMode()}))})},m}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/widgets/text-widgets.js b/wp-admin/js/widgets/text-widgets.js new file mode 100644 index 0000000..48d7247 --- /dev/null +++ b/wp-admin/js/widgets/text-widgets.js @@ -0,0 +1,550 @@ +/** + * @output wp-admin/js/widgets/text-widgets.js + */ + +/* global tinymce, switchEditors */ +/* eslint consistent-this: [ "error", "control" ] */ + +/** + * @namespace wp.textWidgets + */ +wp.textWidgets = ( function( $ ) { + 'use strict'; + + var component = { + dismissedPointers: [], + idBases: [ 'text' ] + }; + + component.TextWidgetControl = Backbone.View.extend(/** @lends wp.textWidgets.TextWidgetControl.prototype */{ + + /** + * View events. + * + * @type {Object} + */ + events: {}, + + /** + * Text widget control. + * + * @constructs wp.textWidgets.TextWidgetControl + * @augments Backbone.View + * @abstract + * + * @param {Object} options - Options. + * @param {jQuery} options.el - Control field container element. + * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. + * + * @return {void} + */ + initialize: function initialize( options ) { + var control = this; + + if ( ! options.el ) { + throw new Error( 'Missing options.el' ); + } + if ( ! options.syncContainer ) { + throw new Error( 'Missing options.syncContainer' ); + } + + Backbone.View.prototype.initialize.call( control, options ); + control.syncContainer = options.syncContainer; + + control.$el.addClass( 'text-widget-fields' ); + control.$el.html( wp.template( 'widget-text-control-fields' ) ); + + control.customHtmlWidgetPointer = control.$el.find( '.wp-pointer.custom-html-widget-pointer' ); + if ( control.customHtmlWidgetPointer.length ) { + control.customHtmlWidgetPointer.find( '.close' ).on( 'click', function( event ) { + event.preventDefault(); + control.customHtmlWidgetPointer.hide(); + $( '#' + control.fields.text.attr( 'id' ) + '-html' ).trigger( 'focus' ); + control.dismissPointers( [ 'text_widget_custom_html' ] ); + }); + control.customHtmlWidgetPointer.find( '.add-widget' ).on( 'click', function( event ) { + event.preventDefault(); + control.customHtmlWidgetPointer.hide(); + control.openAvailableWidgetsPanel(); + }); + } + + control.pasteHtmlPointer = control.$el.find( '.wp-pointer.paste-html-pointer' ); + if ( control.pasteHtmlPointer.length ) { + control.pasteHtmlPointer.find( '.close' ).on( 'click', function( event ) { + event.preventDefault(); + control.pasteHtmlPointer.hide(); + control.editor.focus(); + control.dismissPointers( [ 'text_widget_custom_html', 'text_widget_paste_html' ] ); + }); + } + + control.fields = { + title: control.$el.find( '.title' ), + text: control.$el.find( '.text' ) + }; + + // Sync input fields to hidden sync fields which actually get sent to the server. + _.each( control.fields, function( fieldInput, fieldName ) { + fieldInput.on( 'input change', function updateSyncField() { + var syncInput = control.syncContainer.find( '.sync-input.' + fieldName ); + if ( syncInput.val() !== fieldInput.val() ) { + syncInput.val( fieldInput.val() ); + syncInput.trigger( 'change' ); + } + }); + + // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event. + fieldInput.val( control.syncContainer.find( '.sync-input.' + fieldName ).val() ); + }); + }, + + /** + * Dismiss pointers for Custom HTML widget. + * + * @since 4.8.1 + * + * @param {Array} pointers Pointer IDs to dismiss. + * @return {void} + */ + dismissPointers: function dismissPointers( pointers ) { + _.each( pointers, function( pointer ) { + wp.ajax.post( 'dismiss-wp-pointer', { + pointer: pointer + }); + component.dismissedPointers.push( pointer ); + }); + }, + + /** + * Open available widgets panel. + * + * @since 4.8.1 + * @return {void} + */ + openAvailableWidgetsPanel: function openAvailableWidgetsPanel() { + var sidebarControl; + wp.customize.section.each( function( section ) { + if ( section.extended( wp.customize.Widgets.SidebarSection ) && section.expanded() ) { + sidebarControl = wp.customize.control( 'sidebars_widgets[' + section.params.sidebarId + ']' ); + } + }); + if ( ! sidebarControl ) { + return; + } + setTimeout( function() { // Timeout to prevent click event from causing panel to immediately collapse. + wp.customize.Widgets.availableWidgetsPanel.open( sidebarControl ); + wp.customize.Widgets.availableWidgetsPanel.$search.val( 'HTML' ).trigger( 'keyup' ); + }); + }, + + /** + * Update input fields from the sync fields. + * + * This function is called at the widget-updated and widget-synced events. + * A field will only be updated if it is not currently focused, to avoid + * overwriting content that the user is entering. + * + * @return {void} + */ + updateFields: function updateFields() { + var control = this, syncInput; + + if ( ! control.fields.title.is( document.activeElement ) ) { + syncInput = control.syncContainer.find( '.sync-input.title' ); + control.fields.title.val( syncInput.val() ); + } + + syncInput = control.syncContainer.find( '.sync-input.text' ); + if ( control.fields.text.is( ':visible' ) ) { + if ( ! control.fields.text.is( document.activeElement ) ) { + control.fields.text.val( syncInput.val() ); + } + } else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) { + control.editor.setContent( wp.oldEditor.autop( syncInput.val() ) ); + } + }, + + /** + * Initialize editor. + * + * @return {void} + */ + initializeEditor: function initializeEditor() { + var control = this, changeDebounceDelay = 1000, id, textarea, triggerChangeIfDirty, restoreTextMode = false, needsTextareaChangeTrigger = false, previousValue; + textarea = control.fields.text; + id = textarea.attr( 'id' ); + previousValue = textarea.val(); + + /** + * Trigger change if dirty. + * + * @return {void} + */ + triggerChangeIfDirty = function() { + var updateWidgetBuffer = 300; // See wp.customize.Widgets.WidgetControl._setupUpdateUI() which uses 250ms for updateWidgetDebounced. + if ( control.editor.isDirty() ) { + + /* + * Account for race condition in customizer where user clicks Save & Publish while + * focus was just previously given to the editor. Since updates to the editor + * are debounced at 1 second and since widget input changes are only synced to + * settings after 250ms, the customizer needs to be put into the processing + * state during the time between the change event is triggered and updateWidget + * logic starts. Note that the debounced update-widget request should be able + * to be removed with the removal of the update-widget request entirely once + * widgets are able to mutate their own instance props directly in JS without + * having to make server round-trips to call the respective WP_Widget::update() + * callbacks. See <https://core.trac.wordpress.org/ticket/33507>. + */ + if ( wp.customize && wp.customize.state ) { + wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() + 1 ); + _.delay( function() { + wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() - 1 ); + }, updateWidgetBuffer ); + } + + if ( ! control.editor.isHidden() ) { + control.editor.save(); + } + } + + // Trigger change on textarea when it has changed so the widget can enter a dirty state. + if ( needsTextareaChangeTrigger && previousValue !== textarea.val() ) { + textarea.trigger( 'change' ); + needsTextareaChangeTrigger = false; + previousValue = textarea.val(); + } + }; + + // Just-in-time force-update the hidden input fields. + control.syncContainer.closest( '.widget' ).find( '[name=savewidget]:first' ).on( 'click', function onClickSaveButton() { + triggerChangeIfDirty(); + }); + + /** + * Build (or re-build) the visual editor. + * + * @return {void} + */ + function buildEditor() { + var editor, onInit, showPointerElement; + + // Abort building if the textarea is gone, likely due to the widget having been deleted entirely. + if ( ! document.getElementById( id ) ) { + return; + } + + // The user has disabled TinyMCE. + if ( typeof window.tinymce === 'undefined' ) { + wp.oldEditor.initialize( id, { + quicktags: true, + mediaButtons: true + }); + + return; + } + + // Destroy any existing editor so that it can be re-initialized after a widget-updated event. + if ( tinymce.get( id ) ) { + restoreTextMode = tinymce.get( id ).isHidden(); + wp.oldEditor.remove( id ); + } + + // Add or enable the `wpview` plugin. + $( document ).one( 'wp-before-tinymce-init.text-widget-init', function( event, init ) { + // If somebody has removed all plugins, they must have a good reason. + // Keep it that way. + if ( ! init.plugins ) { + return; + } else if ( ! /\bwpview\b/.test( init.plugins ) ) { + init.plugins += ',wpview'; + } + } ); + + wp.oldEditor.initialize( id, { + tinymce: { + wpautop: true + }, + quicktags: true, + mediaButtons: true + }); + + /** + * Show a pointer, focus on dismiss, and speak the contents for a11y. + * + * @param {jQuery} pointerElement Pointer element. + * @return {void} + */ + showPointerElement = function( pointerElement ) { + pointerElement.show(); + pointerElement.find( '.close' ).trigger( 'focus' ); + wp.a11y.speak( pointerElement.find( 'h3, p' ).map( function() { + return $( this ).text(); + } ).get().join( '\n\n' ) ); + }; + + editor = window.tinymce.get( id ); + if ( ! editor ) { + throw new Error( 'Failed to initialize editor' ); + } + onInit = function() { + + // When a widget is moved in the DOM the dynamically-created TinyMCE iframe will be destroyed and has to be re-built. + $( editor.getWin() ).on( 'pagehide', function() { + _.defer( buildEditor ); + }); + + // If a prior mce instance was replaced, and it was in text mode, toggle to text mode. + if ( restoreTextMode ) { + switchEditors.go( id, 'html' ); + } + + // Show the pointer. + $( '#' + id + '-html' ).on( 'click', function() { + control.pasteHtmlPointer.hide(); // Hide the HTML pasting pointer. + + if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_custom_html' ) ) { + return; + } + showPointerElement( control.customHtmlWidgetPointer ); + }); + + // Hide the pointer when switching tabs. + $( '#' + id + '-tmce' ).on( 'click', function() { + control.customHtmlWidgetPointer.hide(); + }); + + // Show pointer when pasting HTML. + editor.on( 'pastepreprocess', function( event ) { + var content = event.content; + if ( -1 !== component.dismissedPointers.indexOf( 'text_widget_paste_html' ) || ! content || ! /<\w+.*?>/.test( content ) ) { + return; + } + + // Show the pointer after a slight delay so the user sees what they pasted. + _.delay( function() { + showPointerElement( control.pasteHtmlPointer ); + }, 250 ); + }); + }; + + if ( editor.initialized ) { + onInit(); + } else { + editor.on( 'init', onInit ); + } + + control.editorFocused = false; + + editor.on( 'focus', function onEditorFocus() { + control.editorFocused = true; + }); + editor.on( 'paste', function onEditorPaste() { + editor.setDirty( true ); // Because pasting doesn't currently set the dirty state. + triggerChangeIfDirty(); + }); + editor.on( 'NodeChange', function onNodeChange() { + needsTextareaChangeTrigger = true; + }); + editor.on( 'NodeChange', _.debounce( triggerChangeIfDirty, changeDebounceDelay ) ); + editor.on( 'blur hide', function onEditorBlur() { + control.editorFocused = false; + triggerChangeIfDirty(); + }); + + control.editor = editor; + } + + buildEditor(); + } + }); + + /** + * Mapping of widget ID to instances of TextWidgetControl subclasses. + * + * @memberOf wp.textWidgets + * + * @type {Object.<string, wp.textWidgets.TextWidgetControl>} + */ + component.widgetControls = {}; + + /** + * Handle widget being added or initialized for the first time at the widget-added event. + * + * @memberOf wp.textWidgets + * + * @param {jQuery.Event} event - Event. + * @param {jQuery} widgetContainer - Widget container element. + * + * @return {void} + */ + component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { + var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone, fieldContainer, syncContainer; + widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen. + + idBase = widgetForm.find( '> .id_base' ).val(); + if ( -1 === component.idBases.indexOf( idBase ) ) { + return; + } + + // Prevent initializing already-added widgets. + widgetId = widgetForm.find( '.widget-id' ).val(); + if ( component.widgetControls[ widgetId ] ) { + return; + } + + // Bypass using TinyMCE when widget is in legacy mode. + if ( ! widgetForm.find( '.visual' ).val() ) { + return; + } + + /* + * Create a container element for the widget control fields. + * This is inserted into the DOM immediately before the .widget-content + * element because the contents of this element are essentially "managed" + * by PHP, where each widget update cause the entire element to be emptied + * and replaced with the rendered output of WP_Widget::form() which is + * sent back in Ajax request made to save/update the widget instance. + * To prevent a "flash of replaced DOM elements and re-initialized JS + * components", the JS template is rendered outside of the normal form + * container. + */ + fieldContainer = $( '<div></div>' ); + syncContainer = widgetContainer.find( '.widget-content:first' ); + syncContainer.before( fieldContainer ); + + widgetControl = new component.TextWidgetControl({ + el: fieldContainer, + syncContainer: syncContainer + }); + + component.widgetControls[ widgetId ] = widgetControl; + + /* + * Render the widget once the widget parent's container finishes animating, + * as the widget-added event fires with a slideDown of the container. + * This ensures that the textarea is visible and an iframe can be embedded + * with TinyMCE being able to set contenteditable on it. + */ + renderWhenAnimationDone = function() { + if ( ! widgetContainer.hasClass( 'open' ) ) { + setTimeout( renderWhenAnimationDone, animatedCheckDelay ); + } else { + widgetControl.initializeEditor(); + } + }; + renderWhenAnimationDone(); + }; + + /** + * Setup widget in accessibility mode. + * + * @memberOf wp.textWidgets + * + * @return {void} + */ + component.setupAccessibleMode = function setupAccessibleMode() { + var widgetForm, idBase, widgetControl, fieldContainer, syncContainer; + widgetForm = $( '.editwidget > form' ); + if ( 0 === widgetForm.length ) { + return; + } + + idBase = widgetForm.find( '.id_base' ).val(); + if ( -1 === component.idBases.indexOf( idBase ) ) { + return; + } + + // Bypass using TinyMCE when widget is in legacy mode. + if ( ! widgetForm.find( '.visual' ).val() ) { + return; + } + + fieldContainer = $( '<div></div>' ); + syncContainer = widgetForm.find( '> .widget-inside' ); + syncContainer.before( fieldContainer ); + + widgetControl = new component.TextWidgetControl({ + el: fieldContainer, + syncContainer: syncContainer + }); + + widgetControl.initializeEditor(); + }; + + /** + * Sync widget instance data sanitized from server back onto widget model. + * + * This gets called via the 'widget-updated' event when saving a widget from + * the widgets admin screen and also via the 'widget-synced' event when making + * a change to a widget in the customizer. + * + * @memberOf wp.textWidgets + * + * @param {jQuery.Event} event - Event. + * @param {jQuery} widgetContainer - Widget container element. + * @return {void} + */ + component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) { + var widgetForm, widgetId, widgetControl, idBase; + widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); + + idBase = widgetForm.find( '> .id_base' ).val(); + if ( -1 === component.idBases.indexOf( idBase ) ) { + return; + } + + widgetId = widgetForm.find( '> .widget-id' ).val(); + widgetControl = component.widgetControls[ widgetId ]; + if ( ! widgetControl ) { + return; + } + + widgetControl.updateFields(); + }; + + /** + * Initialize functionality. + * + * This function exists to prevent the JS file from having to boot itself. + * When WordPress enqueues this script, it should have an inline script + * attached which calls wp.textWidgets.init(). + * + * @memberOf wp.textWidgets + * + * @return {void} + */ + component.init = function init() { + var $document = $( document ); + $document.on( 'widget-added', component.handleWidgetAdded ); + $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); + + /* + * Manually trigger widget-added events for media widgets on the admin + * screen once they are expanded. The widget-added event is not triggered + * for each pre-existing widget on the widgets admin screen like it is + * on the customizer. Likewise, the customizer only triggers widget-added + * when the widget is expanded to just-in-time construct the widget form + * when it is actually going to be displayed. So the following implements + * the same for the widgets admin screen, to invoke the widget-added + * handler when a pre-existing media widget is expanded. + */ + $( function initializeExistingWidgetContainers() { + var widgetContainers; + if ( 'widgets' !== window.pagenow ) { + return; + } + widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' ); + widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() { + var widgetContainer = $( this ); + component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); + }); + + // Accessibility mode. + component.setupAccessibleMode(); + }); + }; + + return component; +})( jQuery ); diff --git a/wp-admin/js/widgets/text-widgets.min.js b/wp-admin/js/widgets/text-widgets.min.js new file mode 100644 index 0000000..6877ac8 --- /dev/null +++ b/wp-admin/js/widgets/text-widgets.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +wp.textWidgets=function(r){"use strict";var u={dismissedPointers:[],idBases:["text"]};return u.TextWidgetControl=Backbone.View.extend({events:{},initialize:function(e){var n=this;if(!e.el)throw new Error("Missing options.el");if(!e.syncContainer)throw new Error("Missing options.syncContainer");Backbone.View.prototype.initialize.call(n,e),n.syncContainer=e.syncContainer,n.$el.addClass("text-widget-fields"),n.$el.html(wp.template("widget-text-control-fields")),n.customHtmlWidgetPointer=n.$el.find(".wp-pointer.custom-html-widget-pointer"),n.customHtmlWidgetPointer.length&&(n.customHtmlWidgetPointer.find(".close").on("click",function(e){e.preventDefault(),n.customHtmlWidgetPointer.hide(),r("#"+n.fields.text.attr("id")+"-html").trigger("focus"),n.dismissPointers(["text_widget_custom_html"])}),n.customHtmlWidgetPointer.find(".add-widget").on("click",function(e){e.preventDefault(),n.customHtmlWidgetPointer.hide(),n.openAvailableWidgetsPanel()})),n.pasteHtmlPointer=n.$el.find(".wp-pointer.paste-html-pointer"),n.pasteHtmlPointer.length&&n.pasteHtmlPointer.find(".close").on("click",function(e){e.preventDefault(),n.pasteHtmlPointer.hide(),n.editor.focus(),n.dismissPointers(["text_widget_custom_html","text_widget_paste_html"])}),n.fields={title:n.$el.find(".title"),text:n.$el.find(".text")},_.each(n.fields,function(t,i){t.on("input change",function(){var e=n.syncContainer.find(".sync-input."+i);e.val()!==t.val()&&(e.val(t.val()),e.trigger("change"))}),t.val(n.syncContainer.find(".sync-input."+i).val())})},dismissPointers:function(e){_.each(e,function(e){wp.ajax.post("dismiss-wp-pointer",{pointer:e}),u.dismissedPointers.push(e)})},openAvailableWidgetsPanel:function(){var t;wp.customize.section.each(function(e){e.extended(wp.customize.Widgets.SidebarSection)&&e.expanded()&&(t=wp.customize.control("sidebars_widgets["+e.params.sidebarId+"]"))}),t&&setTimeout(function(){wp.customize.Widgets.availableWidgetsPanel.open(t),wp.customize.Widgets.availableWidgetsPanel.$search.val("HTML").trigger("keyup")})},updateFields:function(){var e,t=this;t.fields.title.is(document.activeElement)||(e=t.syncContainer.find(".sync-input.title"),t.fields.title.val(e.val())),e=t.syncContainer.find(".sync-input.text"),t.fields.text.is(":visible")?t.fields.text.is(document.activeElement)||t.fields.text.val(e.val()):t.editor&&!t.editorFocused&&e.val()!==t.fields.text.val()&&t.editor.setContent(wp.oldEditor.autop(e.val()))},initializeEditor:function(){var d,e,o,t,s=this,a=1e3,l=!1,c=!1;e=s.fields.text,d=e.attr("id"),t=e.val(),o=function(){s.editor.isDirty()&&(wp.customize&&wp.customize.state&&(wp.customize.state("processing").set(wp.customize.state("processing").get()+1),_.delay(function(){wp.customize.state("processing").set(wp.customize.state("processing").get()-1)},300)),s.editor.isHidden()||s.editor.save()),c&&t!==e.val()&&(e.trigger("change"),c=!1,t=e.val())},s.syncContainer.closest(".widget").find("[name=savewidget]:first").on("click",function(){o()}),function e(){var t,i,n;if(document.getElementById(d))if(void 0===window.tinymce)wp.oldEditor.initialize(d,{quicktags:!0,mediaButtons:!0});else{if(tinymce.get(d)&&(l=tinymce.get(d).isHidden(),wp.oldEditor.remove(d)),r(document).one("wp-before-tinymce-init.text-widget-init",function(e,t){t.plugins&&!/\bwpview\b/.test(t.plugins)&&(t.plugins+=",wpview")}),wp.oldEditor.initialize(d,{tinymce:{wpautop:!0},quicktags:!0,mediaButtons:!0}),n=function(e){e.show(),e.find(".close").trigger("focus"),wp.a11y.speak(e.find("h3, p").map(function(){return r(this).text()}).get().join("\n\n"))},!(t=window.tinymce.get(d)))throw new Error("Failed to initialize editor");i=function(){r(t.getWin()).on("pagehide",function(){_.defer(e)}),l&&switchEditors.go(d,"html"),r("#"+d+"-html").on("click",function(){s.pasteHtmlPointer.hide(),-1===u.dismissedPointers.indexOf("text_widget_custom_html")&&n(s.customHtmlWidgetPointer)}),r("#"+d+"-tmce").on("click",function(){s.customHtmlWidgetPointer.hide()}),t.on("pastepreprocess",function(e){e=e.content,-1===u.dismissedPointers.indexOf("text_widget_paste_html")&&e&&/<\w+.*?>/.test(e)&&_.delay(function(){n(s.pasteHtmlPointer)},250)})},t.initialized?i():t.on("init",i),s.editorFocused=!1,t.on("focus",function(){s.editorFocused=!0}),t.on("paste",function(){t.setDirty(!0),o()}),t.on("NodeChange",function(){c=!0}),t.on("NodeChange",_.debounce(o,a)),t.on("blur hide",function(){s.editorFocused=!1,o()}),s.editor=t}}()}}),u.widgetControls={},u.handleWidgetAdded=function(e,t){var i,n,d,o=t.find("> .widget-inside > .form, > .widget-inside > form"),s=o.find("> .id_base").val();-1===u.idBases.indexOf(s)||(s=o.find(".widget-id").val(),u.widgetControls[s])||o.find(".visual").val()&&(o=r("<div></div>"),(d=t.find(".widget-content:first")).before(o),i=new u.TextWidgetControl({el:o,syncContainer:d}),u.widgetControls[s]=i,(n=function(){t.hasClass("open")?i.initializeEditor():setTimeout(n,50)})())},u.setupAccessibleMode=function(){var e,t=r(".editwidget > form");0!==t.length&&(e=t.find(".id_base").val(),-1!==u.idBases.indexOf(e))&&t.find(".visual").val()&&(e=r("<div></div>"),(t=t.find("> .widget-inside")).before(e),new u.TextWidgetControl({el:e,syncContainer:t}).initializeEditor())},u.handleWidgetUpdated=function(e,t){var t=t.find("> .widget-inside > .form, > .widget-inside > form"),i=t.find("> .id_base").val();-1!==u.idBases.indexOf(i)&&(i=t.find("> .widget-id").val(),t=u.widgetControls[i])&&t.updateFields()},u.init=function(){var e=r(document);e.on("widget-added",u.handleWidgetAdded),e.on("widget-synced widget-updated",u.handleWidgetUpdated),r(function(){"widgets"===window.pagenow&&(r(".widgets-holder-wrap:not(#available-widgets)").find("div.widget").one("click.toggle-widget-expanded",function(){var e=r(this);u.handleWidgetAdded(new jQuery.Event("widget-added"),e)}),u.setupAccessibleMode())})},u}(jQuery);
\ No newline at end of file diff --git a/wp-admin/js/word-count.js b/wp-admin/js/word-count.js new file mode 100644 index 0000000..066fc58 --- /dev/null +++ b/wp-admin/js/word-count.js @@ -0,0 +1,220 @@ +/** + * Word or character counting functionality. Count words or characters in a + * provided text string. + * + * @namespace wp.utils + * + * @since 2.6.0 + * @output wp-admin/js/word-count.js + */ + +( function() { + /** + * Word counting utility + * + * @namespace wp.utils.wordcounter + * @memberof wp.utils + * + * @class + * + * @param {Object} settings Optional. Key-value object containing overrides for + * settings. + * @param {RegExp} settings.HTMLRegExp Optional. Regular expression to find HTML elements. + * @param {RegExp} settings.HTMLcommentRegExp Optional. Regular expression to find HTML comments. + * @param {RegExp} settings.spaceRegExp Optional. Regular expression to find irregular space + * characters. + * @param {RegExp} settings.HTMLEntityRegExp Optional. Regular expression to find HTML entities. + * @param {RegExp} settings.connectorRegExp Optional. Regular expression to find connectors that + * split words. + * @param {RegExp} settings.removeRegExp Optional. Regular expression to find remove unwanted + * characters to reduce false-positives. + * @param {RegExp} settings.astralRegExp Optional. Regular expression to find unwanted + * characters when searching for non-words. + * @param {RegExp} settings.wordsRegExp Optional. Regular expression to find words by spaces. + * @param {RegExp} settings.characters_excluding_spacesRegExp Optional. Regular expression to find characters which + * are non-spaces. + * @param {RegExp} settings.characters_including_spacesRegExp Optional. Regular expression to find characters + * including spaces. + * @param {RegExp} settings.shortcodesRegExp Optional. Regular expression to find shortcodes. + * @param {Object} settings.l10n Optional. Localization object containing specific + * configuration for the current localization. + * @param {string} settings.l10n.type Optional. Method of finding words to count. + * @param {Array} settings.l10n.shortcodes Optional. Array of shortcodes that should be removed + * from the text. + * + * @return {void} + */ + function WordCounter( settings ) { + var key, + shortcodes; + + // Apply provided settings to object settings. + if ( settings ) { + for ( key in settings ) { + + // Only apply valid settings. + if ( settings.hasOwnProperty( key ) ) { + this.settings[ key ] = settings[ key ]; + } + } + } + + shortcodes = this.settings.l10n.shortcodes; + + // If there are any localization shortcodes, add this as type in the settings. + if ( shortcodes && shortcodes.length ) { + this.settings.shortcodesRegExp = new RegExp( '\\[\\/?(?:' + shortcodes.join( '|' ) + ')[^\\]]*?\\]', 'g' ); + } + } + + // Default settings. + WordCounter.prototype.settings = { + HTMLRegExp: /<\/?[a-z][^>]*?>/gi, + HTMLcommentRegExp: /<!--[\s\S]*?-->/g, + spaceRegExp: / | /gi, + HTMLEntityRegExp: /&\S+?;/g, + + // \u2014 = em-dash. + connectorRegExp: /--|\u2014/g, + + // Characters to be removed from input text. + removeRegExp: new RegExp( [ + '[', + + // Basic Latin (extract). + '\u0021-\u0040\u005B-\u0060\u007B-\u007E', + + // Latin-1 Supplement (extract). + '\u0080-\u00BF\u00D7\u00F7', + + /* + * The following range consists of: + * General Punctuation + * Superscripts and Subscripts + * Currency Symbols + * Combining Diacritical Marks for Symbols + * Letterlike Symbols + * Number Forms + * Arrows + * Mathematical Operators + * Miscellaneous Technical + * Control Pictures + * Optical Character Recognition + * Enclosed Alphanumerics + * Box Drawing + * Block Elements + * Geometric Shapes + * Miscellaneous Symbols + * Dingbats + * Miscellaneous Mathematical Symbols-A + * Supplemental Arrows-A + * Braille Patterns + * Supplemental Arrows-B + * Miscellaneous Mathematical Symbols-B + * Supplemental Mathematical Operators + * Miscellaneous Symbols and Arrows + */ + '\u2000-\u2BFF', + + // Supplemental Punctuation. + '\u2E00-\u2E7F', + ']' + ].join( '' ), 'g' ), + + // Remove UTF-16 surrogate points, see https://en.wikipedia.org/wiki/UTF-16#U.2BD800_to_U.2BDFFF + astralRegExp: /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + wordsRegExp: /\S\s+/g, + characters_excluding_spacesRegExp: /\S/g, + + /* + * Match anything that is not a formatting character, excluding: + * \f = form feed + * \n = new line + * \r = carriage return + * \t = tab + * \v = vertical tab + * \u00AD = soft hyphen + * \u2028 = line separator + * \u2029 = paragraph separator + */ + characters_including_spacesRegExp: /[^\f\n\r\t\v\u00AD\u2028\u2029]/g, + l10n: window.wordCountL10n || {} + }; + + /** + * Counts the number of words (or other specified type) in the specified text. + * + * @since 2.6.0 + * + * @memberof wp.utils.wordcounter + * + * @param {string} text Text to count elements in. + * @param {string} type Optional. Specify type to use. + * + * @return {number} The number of items counted. + */ + WordCounter.prototype.count = function( text, type ) { + var count = 0; + + // Use default type if none was provided. + type = type || this.settings.l10n.type; + + // Sanitize type to one of three possibilities: 'words', 'characters_excluding_spaces' or 'characters_including_spaces'. + if ( type !== 'characters_excluding_spaces' && type !== 'characters_including_spaces' ) { + type = 'words'; + } + + // If we have any text at all. + if ( text ) { + text = text + '\n'; + + // Replace all HTML with a new-line. + text = text.replace( this.settings.HTMLRegExp, '\n' ); + + // Remove all HTML comments. + text = text.replace( this.settings.HTMLcommentRegExp, '' ); + + // If a shortcode regular expression has been provided use it to remove shortcodes. + if ( this.settings.shortcodesRegExp ) { + text = text.replace( this.settings.shortcodesRegExp, '\n' ); + } + + // Normalize non-breaking space to a normal space. + text = text.replace( this.settings.spaceRegExp, ' ' ); + + if ( type === 'words' ) { + + // Remove HTML Entities. + text = text.replace( this.settings.HTMLEntityRegExp, '' ); + + // Convert connectors to spaces to count attached text as words. + text = text.replace( this.settings.connectorRegExp, ' ' ); + + // Remove unwanted characters. + text = text.replace( this.settings.removeRegExp, '' ); + } else { + + // Convert HTML Entities to "a". + text = text.replace( this.settings.HTMLEntityRegExp, 'a' ); + + // Remove surrogate points. + text = text.replace( this.settings.astralRegExp, 'a' ); + } + + // Match with the selected type regular expression to count the items. + text = text.match( this.settings[ type + 'RegExp' ] ); + + // If we have any matches, set the count to the number of items found. + if ( text ) { + count = text.length; + } + } + + return count; + }; + + // Add the WordCounter to the WP Utils. + window.wp = window.wp || {}; + window.wp.utils = window.wp.utils || {}; + window.wp.utils.WordCounter = WordCounter; +} )(); diff --git a/wp-admin/js/word-count.min.js b/wp-admin/js/word-count.min.js new file mode 100644 index 0000000..b1d1bf8 --- /dev/null +++ b/wp-admin/js/word-count.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +!function(){function e(e){var t,s;if(e)for(t in e)e.hasOwnProperty(t)&&(this.settings[t]=e[t]);(s=this.settings.l10n.shortcodes)&&s.length&&(this.settings.shortcodesRegExp=new RegExp("\\[\\/?(?:"+s.join("|")+")[^\\]]*?\\]","g"))}e.prototype.settings={HTMLRegExp:/<\/?[a-z][^>]*?>/gi,HTMLcommentRegExp:/<!--[\s\S]*?-->/g,spaceRegExp:/ | /gi,HTMLEntityRegExp:/&\S+?;/g,connectorRegExp:/--|\u2014/g,removeRegExp:new RegExp(["[","!-@[-`{-~","\x80-\xbf\xd7\xf7","\u2000-\u2bff","\u2e00-\u2e7f","]"].join(""),"g"),astralRegExp:/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,wordsRegExp:/\S\s+/g,characters_excluding_spacesRegExp:/\S/g,characters_including_spacesRegExp:/[^\f\n\r\t\v\u00AD\u2028\u2029]/g,l10n:window.wordCountL10n||{}},e.prototype.count=function(e,t){var s=0;return"characters_excluding_spaces"!==(t=t||this.settings.l10n.type)&&"characters_including_spaces"!==t&&(t="words"),s=e&&(e=(e=(e+="\n").replace(this.settings.HTMLRegExp,"\n")).replace(this.settings.HTMLcommentRegExp,""),e=(e=this.settings.shortcodesRegExp?e.replace(this.settings.shortcodesRegExp,"\n"):e).replace(this.settings.spaceRegExp," "),e=(e="words"===t?(e=(e=e.replace(this.settings.HTMLEntityRegExp,"")).replace(this.settings.connectorRegExp," ")).replace(this.settings.removeRegExp,""):(e=e.replace(this.settings.HTMLEntityRegExp,"a")).replace(this.settings.astralRegExp,"a")).match(this.settings[t+"RegExp"]))?e.length:s},window.wp=window.wp||{},window.wp.utils=window.wp.utils||{},window.wp.utils.WordCounter=e}();
\ No newline at end of file diff --git a/wp-admin/js/xfn.js b/wp-admin/js/xfn.js new file mode 100644 index 0000000..cf7fcf8 --- /dev/null +++ b/wp-admin/js/xfn.js @@ -0,0 +1,23 @@ +/** + * 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 ) ); + }); +}); diff --git a/wp-admin/js/xfn.min.js b/wp-admin/js/xfn.min.js new file mode 100644 index 0000000..cdd7cad --- /dev/null +++ b/wp-admin/js/xfn.min.js @@ -0,0 +1,2 @@ +/*! This file is auto-generated */ +jQuery(function(l){l("#link_rel").prop("readonly",!0),l("#linkxfndiv input").on("click keyup",function(){var e=l("#me").is(":checked"),i="";l("input.valinp").each(function(){e?l(this).prop("disabled",!0).parent().addClass("disabled"):(l(this).removeAttr("disabled").parent().removeClass("disabled"),l(this).is(":checked")&&""!==l(this).val()&&(i+=l(this).val()+" "))}),l("#link_rel").val(e?"me":i.substr(0,i.length-1))})});
\ No newline at end of file |