diff --git a/docs/reference/classes/WP-Backstage-Component.html b/docs/reference/classes/WP-Backstage-Component.html index 9a29862..64bbba3 100644 --- a/docs/reference/classes/WP-Backstage-Component.html +++ b/docs/reference/classes/WP-Backstage-Component.html @@ -4700,7 +4700,7 @@

@@ -5297,7 +5297,7 @@

@@ -5351,6 +5351,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -5501,7 +5511,7 @@

@@ -5572,7 +5582,7 @@

diff --git a/docs/reference/classes/WP-Backstage-Nav-Menu-Item.html b/docs/reference/classes/WP-Backstage-Nav-Menu-Item.html index e6de7ae..0fc9ed8 100644 --- a/docs/reference/classes/WP-Backstage-Nav-Menu-Item.html +++ b/docs/reference/classes/WP-Backstage-Nav-Menu-Item.html @@ -2645,7 +2645,7 @@

class-wp-backstage-nav-menu-item.php : - 509 + 507 @@ -2717,7 +2717,7 @@

class-wp-backstage-nav-menu-item.php : - 475 + 473 @@ -4622,7 +4622,7 @@

@@ -5475,7 +5475,7 @@

@@ -6072,7 +6072,7 @@

@@ -6126,6 +6126,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -6276,7 +6286,7 @@

@@ -6347,7 +6357,7 @@

diff --git a/docs/reference/classes/WP-Backstage-Options.html b/docs/reference/classes/WP-Backstage-Options.html index fa0d5a8..50ccffb 100644 --- a/docs/reference/classes/WP-Backstage-Options.html +++ b/docs/reference/classes/WP-Backstage-Options.html @@ -5666,7 +5666,7 @@

@@ -6263,7 +6263,7 @@

@@ -6317,6 +6317,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -6467,7 +6477,7 @@

@@ -6538,7 +6548,7 @@

diff --git a/docs/reference/classes/WP-Backstage-Post-Type.html b/docs/reference/classes/WP-Backstage-Post-Type.html index d7ac47f..525c401 100644 --- a/docs/reference/classes/WP-Backstage-Post-Type.html +++ b/docs/reference/classes/WP-Backstage-Post-Type.html @@ -6545,7 +6545,7 @@

@@ -7142,7 +7142,7 @@

@@ -7196,6 +7196,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -7346,7 +7356,7 @@

@@ -7417,7 +7427,7 @@

diff --git a/docs/reference/classes/WP-Backstage-Taxonomy.html b/docs/reference/classes/WP-Backstage-Taxonomy.html index 06ff3c1..10ee605 100644 --- a/docs/reference/classes/WP-Backstage-Taxonomy.html +++ b/docs/reference/classes/WP-Backstage-Taxonomy.html @@ -5946,7 +5946,7 @@

@@ -6543,7 +6543,7 @@

@@ -6597,6 +6597,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -6747,7 +6757,7 @@

@@ -6818,7 +6828,7 @@

diff --git a/docs/reference/classes/WP-Backstage-User.html b/docs/reference/classes/WP-Backstage-User.html index 18c4d77..2214e2b 100644 --- a/docs/reference/classes/WP-Backstage-User.html +++ b/docs/reference/classes/WP-Backstage-User.html @@ -5598,7 +5598,7 @@

@@ -6273,7 +6273,7 @@

@@ -6327,6 +6327,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -6477,7 +6487,7 @@

@@ -6548,7 +6558,7 @@

diff --git a/docs/reference/classes/WP-Backstage-Widget.html b/docs/reference/classes/WP-Backstage-Widget.html index bde5b2d..5942f49 100644 --- a/docs/reference/classes/WP-Backstage-Widget.html +++ b/docs/reference/classes/WP-Backstage-Widget.html @@ -5283,7 +5283,7 @@

@@ -5880,7 +5880,7 @@

@@ -5934,6 +5934,16 @@

Full rewrite of the media uploader markup.

+ +
+ since +
+
+ 3.3.0 + +

Adds error and loader elements and removes the clone methodology in favor of ajax rendering.

+
+
@@ -6084,7 +6094,7 @@

@@ -6155,7 +6165,7 @@

diff --git a/docs/reference/classes/WP-Backstage.html b/docs/reference/classes/WP-Backstage.html index a65e1e5..f98123a 100644 --- a/docs/reference/classes/WP-Backstage.html +++ b/docs/reference/classes/WP-Backstage.html @@ -179,6 +179,13 @@

Add Help Tab
+
+ ajax_render_media() + +  : void +
+
Ajax Render Media
+
enqueue_admin_scripts() @@ -270,6 +277,13 @@

Inline Global Style
+
+ inline_media_mixin_overrides_script() + +  : void +
+
Inline Media Mixin Overrides Script
+
inline_media_uploader_script() @@ -361,6 +375,13 @@

Render Help Tab
+
+ render_media_item() + +  : void +
+
Render Media Item
+
is_plugin_active() @@ -674,7 +695,7 @@

@@ -737,6 +758,56 @@

Return values
— + +
+

+ ajax_render_media() + +

+ + +

Ajax Render Media

+ + + public + ajax_render_media() : void + +

This method is responsible for providing the ajax response for the media uploader +preview.

+
+ + + +
+ Tags + +
+
+
+ since +
+
+ 3.3.0 + + +
+
+ +
Return values
+ void + — + +
@@ -867,7 +938,7 @@

@@ -1031,7 +1102,7 @@

@@ -1090,7 +1161,7 @@

@@ -1190,7 +1261,7 @@

@@ -1239,7 +1310,7 @@

@@ -1330,7 +1401,7 @@

@@ -1379,7 +1450,7 @@

@@ -1470,7 +1541,7 @@

@@ -1578,7 +1649,7 @@

@@ -1627,7 +1698,7 @@

@@ -1676,7 +1747,7 @@

@@ -1709,6 +1780,62 @@

Return values
— +
+
+

+ inline_media_mixin_overrides_script() + +

+ + +

Inline Media Mixin Overrides Script

+ + + public + inline_media_mixin_overrides_script() : void + +

This method is responsible for outputting a script that makes a small override to the native +WordPress wp.media.mixin.removeAllPlayers() mixin. By default, WordPress kills all media +elements when one is rendered in the WordPress media modal, with no way to ignore that behavior. +The side effect of this is that media elements in the WP Admin are killed when selecting an audio +or video attachment in the media uploader. Because WP Backstage uses WordPress media elements on +audio and video previews in the media uploader fields, it is necessary to provide a way to ignore +these in this function. This function was copied from /wp-includes/js/media-audiovideo.js, and +adds a simple check for a wp-mediaelement-keep class to ignore removal.

+
+ + + +
+ Tags + +
+
+
+ since +
+
+ 3.3.0 + + +
+
+ +
Return values
+ void + — + +
class-wp-backstage.php : - 683 + 884 @@ -1800,6 +1927,16 @@

Full rewrite of the media uploader script.

+ +
+ since +
+
+ 3.3.0 + +

Renders previews via ajax instead of clone functionality.

+
+
@@ -1822,7 +1959,7 @@

@@ -1849,6 +1986,16 @@

2.0.0 + +
+ since +
+
+ 3.3.0 + +

Adds styles for new media uploader preview rendering.

+
+
@@ -1871,7 +2018,7 @@

class-wp-backstage.php : - 1874 + 2133 @@ -1934,7 +2081,7 @@

@@ -1981,7 +2128,7 @@

@@ -2028,7 +2175,7 @@

@@ -2075,7 +2222,7 @@

@@ -2122,7 +2269,7 @@

class-wp-backstage.php : - 587 + 779 @@ -2169,7 +2316,7 @@

@@ -2216,7 +2363,7 @@

class-wp-backstage.php : - 2445 + 2709 @@ -2263,7 +2410,7 @@

@@ -2310,7 +2457,7 @@

@@ -2365,7 +2512,7 @@

@@ -2416,6 +2563,78 @@

Return values
— +
+
+

+ render_media_item() + +

+ + +

Render Media Item

+ + + public + render_media_item(int $attachment_id[, bool $is_multiple = false ]) : void + +

This method is responsible for rendering a single media item for use in the media uploader +field. Taking into account whether the media uploader is rendering multiple items or not, +and then taking into account the mime type of the attachment, render a media element. If

+
+ +
Parameters
+
+
+ $attachment_id + : int +
+
+

The ID of the attachment to render.

+
+ +
+
+ $is_multiple + : bool + = false
+
+

Whether the media uploader allows multiple attachments or not.

+
+ +
+
+ + +
+ Tags + +
+
+
+ since +
+
+ 3.3.0 + + +
+
+ +
Return values
+ void + — + +
@@ -2501,7 +2720,7 @@

diff --git a/docs/reference/js/searchIndex.js b/docs/reference/js/searchIndex.js index a712407..8153844 100644 --- a/docs/reference/js/searchIndex.js +++ b/docs/reference/js/searchIndex.js @@ -1125,6 +1125,11 @@ Search.appendIndex( "name": "enqueue_admin_scripts", "summary": "Enqueue\u0020Admin\u0020Scripts", "url": "classes/WP-Backstage.html#method_enqueue_admin_scripts" + }, { + "fqsen": "\\WP_Backstage\u003A\u003Ainline_media_mixin_overrides_script\u0028\u0029", + "name": "inline_media_mixin_overrides_script", + "summary": "Inline\u0020Media\u0020Mixin\u0020Overrides\u0020Script", + "url": "classes/WP-Backstage.html#method_inline_media_mixin_overrides_script" }, { "fqsen": "\\WP_Backstage\u003A\u003Ainline_thumbnail_column_style\u0028\u0029", "name": "inline_thumbnail_column_style", @@ -1210,6 +1215,16 @@ Search.appendIndex( "name": "inline_user_script", "summary": "Inline\u0020User\u0020Script", "url": "classes/WP-Backstage.html#method_inline_user_script" + }, { + "fqsen": "\\WP_Backstage\u003A\u003Arender_media_item\u0028\u0029", + "name": "render_media_item", + "summary": "Render\u0020Media\u0020Item", + "url": "classes/WP-Backstage.html#method_render_media_item" + }, { + "fqsen": "\\WP_Backstage\u003A\u003Aajax_render_media\u0028\u0029", + "name": "ajax_render_media", + "summary": "Ajax\u0020Render\u0020Media", + "url": "classes/WP-Backstage.html#method_ajax_render_media" }, { "fqsen": "\\WP_Backstage\u003A\u003A\u0024errors", "name": "errors", diff --git a/docs/reference/reports/markers.html b/docs/reference/reports/markers.html index 3f2af1f..480e96f 100644 --- a/docs/reference/reports/markers.html +++ b/docs/reference/reports/markers.html @@ -126,7 +126,7 @@

class-wp-backsta TODO - 419 + 417 Though it's probably not needed, try to check nonce here. @@ -203,12 +203,12 @@

class-wp-backstage.php< TODO - 1467 + 1726 Figure out why tiny MCE is only focusing the last editor on the page, TODO - 1436 + 1695 Make sure clicking on label focuses the editor. diff --git a/includes/class-wp-backstage-component.php b/includes/class-wp-backstage-component.php index daa7ddf..273fe35 100644 --- a/includes/class-wp-backstage-component.php +++ b/includes/class-wp-backstage-component.php @@ -2847,6 +2847,7 @@ class="description" * * @since 0.0.1 * @since 2.0.0 Full rewrite of the media uploader markup. + * @since 3.3.0 Adds error and loader elements and removes the `clone` methodology in favor of ajax rendering. * @param array $field An array of field arguments. * @return void */ @@ -2887,7 +2888,10 @@ protected function render_media_uploader( $field = array() ) { /* translators: 1: field label */ _x( 'Remove %1$s', 'media uploader field - remove button', 'wp_backstage' ), $field['label'] - ); ?> + ); + $loader_text = _x( 'Loading preview...', 'media uploader field - loader', 'wp_backstage' ); + $error_text = _x( 'There was an error rendering the preview.', 'media uploader field - error', 'wp_backstage' ); + $try_again_button_text = _x( 'Try again', 'media uploader field - try again button', 'wp_backstage' ) ?> + - - - + id="" + class="wp-backstage-media-uploader__error notice inline notice-error"> + + + + + @@ -2983,6 +2989,16 @@ class="wp-backstage-media-uploader__button wp-backstage-media-uploader__button-- + + <?php echo esc_attr( $loader_text ); ?> +   + + + non_regular_text_fields ) ) { @@ -385,8 +385,6 @@ public function save( $menu_id = 0, $item_id = 0, $menu_item_data = array() ) { if ( is_array( $fields ) && ! empty( $fields ) ) { - $values = array(); - foreach ( $fields as $field ) { if ( isset( $post_data[ $field['name'] ][ $item_id ] ) ) { diff --git a/includes/class-wp-backstage.php b/includes/class-wp-backstage.php index 6e582f2..61e558c 100644 --- a/includes/class-wp-backstage.php +++ b/includes/class-wp-backstage.php @@ -167,6 +167,7 @@ public function init() { add_action( 'admin_print_styles', array( $this, 'inline_media_uploader_style' ), 10 ); add_action( 'admin_print_styles', array( $this, 'inline_thumbnail_column_style' ), 10 ); add_action( 'admin_print_scripts', array( $this, 'inline_global_script' ), 10 ); + add_action( 'admin_print_footer_scripts', array( $this, 'inline_media_mixin_overrides_script' ), 20 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ), 10 ); add_action( 'admin_print_footer_scripts', array( $this, 'inline_media_uploader_script' ), 10 ); add_action( 'admin_print_footer_scripts', array( $this, 'inline_date_picker_script' ), 10 ); @@ -187,6 +188,7 @@ public function init() { add_action( 'customize_controls_print_scripts', array( $this, 'inline_widget_customizer_script' ), 10 ); add_action( 'customize_controls_print_scripts', array( $this, 'inline_nav_menu_item_customizer_script' ), 10 ); add_action( 'wp_backstage_options_print_footer_scripts', array( $this, 'inline_options_script' ), 10 ); + add_action( 'wp_ajax_wp_backstage_render_media', array( $this, 'ajax_render_media' ), 10 ); } /** @@ -358,6 +360,7 @@ public function add_help_tab( $screen = null ) { * Inlines the media uploader field style. * * @since 2.0.0 + * @since 3.3.0 Adds styles for new media uploader preview rendering. * @return void */ public function inline_media_uploader_style() { ?> @@ -411,27 +414,175 @@ public function inline_media_uploader_style() { ?> display: block; position: relative; padding: 0; - max-width: 100%; + width: 150px; + height: 150px; margin: 0 12px 12px 0; float: left; - background-color: #ffffff; + color: #3c434a; + background: #f0f0f1; + font-size: 12px; + box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.10), inset 0 0 0 1px rgba(0, 0, 0, 0.5); + cursor: move; } - .wp-backstage-media-uploader__attachment[data-attachment-id="0"] { - display: none !important; + #addtag .wp-backstage-media-uploader__attachment, + .widget .wp-backstage-media-uploader__attachment, + .menu-item .wp-backstage-media-uploader__attachment { + width: 115px; + height: 115px; } - .wp-backstage-media-uploader__attachment-image { + .wp-backstage-media-uploader__attachment > img { display: block; width: 100%; + height: 100%; + object-fit: scale-down; margin: 0; padding: 0; + box-shadow: inset 0 0 0 1px #dcdcde; + } + + .wp-backstage-media-uploader__attachment[data-attachment-type="image"] > img { + object-fit: contain; + background-color: #ffffff; + background-position: 0 0, 10px 10px; + background-size: 20px 20px; background-image: linear-gradient(45deg,#c3c4c7 25%,transparent 25%,transparent 75%,#c3c4c7 75%,#c3c4c7), linear-gradient(45deg,#c3c4c7 25%,transparent 25%,transparent 75%,#c3c4c7 75%,#c3c4c7); + } + + .wp-backstage-media-uploader__attachment-filename { + display: none; + position: absolute; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; + max-height: 100%; + word-wrap: break-word; + text-align: center; + padding: 5px 10px; + font-weight: 600; + max-height: calc(100% - 32px); + background: rgba(255, 255, 255, 0.8); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); + overflow-y: auto; + box-sizing: border-box; + } + + .wp-backstage-media-uploader__attachment:not([data-attachment-type="image"]) .wp-backstage-media-uploader__attachment-filename { + display: block; + } + + .wp-backstage-media-uploader__attachment-single-file { + width: 100%; + height: auto; + max-width: 500px; + margin-bottom: 12px; + display: block; + } + + .wp-backstage-media-uploader__attachment-single-file::before, + .wp-backstage-media-uploader__attachment-single-file::after { + content: ''; + width: 100%; + display: table; + clear: both; + } + + .wp-backstage-media-uploader__attachment-single-file > img { + max-width: 100px; + height: auto; + display: block; + float: left; + margin-right: 12px; + } + + .wp-backstage-media-uploader__attachment-single-file-filename { + display: block; + font-weight: 600; + margin-bottom: 0.125em; + word-wrap: break-word; + } + + .wp-backstage-media-uploader__attachment-single-file-meta { + display: block; + font-size: 0.875em; + opacity: 0.75; + } + + .wp-backstage-media-uploader__attachment-single-image { + width: 100%; + height: auto; + max-width: 350px; + margin-bottom: 12px; + display: block; + } + + .wp-backstage-media-uploader__attachment-single-image > img { + max-width: 100%; + height: auto; + display: block; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); + background-color: #ffffff; background-position: 0 0, 10px 10px; - box-shadow: inset 0 0 0 1px #dcdcde; background-size: 20px 20px; + background-image: + linear-gradient(45deg,#c3c4c7 25%,transparent 25%,transparent 75%,#c3c4c7 75%,#c3c4c7), + linear-gradient(45deg,#c3c4c7 25%,transparent 25%,transparent 75%,#c3c4c7 75%,#c3c4c7); + } + + .wp-backstage-media-uploader__attachment-single-image[data-attachment-subtype="svg+xml"] > img { + width: 100%; + } + + .wp-backstage-media-uploader__attachment-single-video { + width: 100%; + height: auto; + max-width: 500px; + margin-bottom: 12px; + display: block; + } + + .wp-backstage-media-uploader__attachment-single-audio { + width: 100%; + height: auto; + max-width: 500px; + margin-bottom: 12px; + display: block; + } + + .wp-backstage-media-uploader__error { + display: none; + } + + .wp-backstage-media-uploader__error > span{ + display: inline-block; + vertical-align: middle; + margin: 0.5em 0; + padding: 2px; + } + + .wp-backstage-media-uploader__loader { + display: none; + } + + .wp-backstage-media-uploader__loader > span { + display: inline-block; + vertical-align: middle; + font-style: italic; + opacity: 0.75; + padding: 0.5em 0; + } + + .wp-backstage-media-uploader__loader > img { + display: inline-block; + vertical-align: middle; + } + + .wp-backstage-media-uploader__try-again { + line-height: inherit; } @@ -568,6 +719,12 @@ public function enqueue_admin_scripts() { if ( ! did_action( 'wp_enqueue_media' ) ) { wp_enqueue_media(); } + if ( ! wp_style_is( 'wp-mediaelement', 'enqueued' ) ) { + wp_enqueue_style( 'wp-mediaelement' ); + } + if ( ! wp_script_is( 'wp-mediaelement', 'enqueued' ) ) { + wp_enqueue_script( 'wp-mediaelement' ); + } // color picker. if ( ! wp_script_is( 'wp-color-picker', 'enqueued' ) ) { wp_enqueue_script( 'wp-color-picker' ); @@ -578,6 +735,41 @@ public function enqueue_admin_scripts() { } + /** + * Inline Media Mixin Overrides Script + * + * This method is responsible for outputting a script that makes a small override to the native + * WordPress `wp.media.mixin.removeAllPlayers()` mixin. By default, WordPress kills all media + * elements when one is rendered in the WordPress media modal, with no way to ignore that behavior. + * The side effect of this is that media elements in the WP Admin are killed when selecting an audio + * or video attachment in the media uploader. Because WP Backstage uses WordPress media elements on + * audio and video previews in the media uploader fields, it is necessary to provide a way to ignore + * these in this function. This function was copied from `/wp-includes/js/media-audiovideo.js`, and + * adds a simple check for a `wp-mediaelement-keep` class to ignore removal. + * + * @since 3.3.0 + * @return void + */ + public function inline_media_mixin_overrides_script() { ?> + + display: none; } + /* Form Tables */ + @media screen and (max-width: 782px) { + .form-table { + table-layout: fixed; + max-width: 100%; + } + } + * * @since 0.0.1 * @since 2.0.0 Full rewrite of the media uploader script. + * @since 3.3.0 Renders previews via ajax instead of clone functionality. * @return void */ public function inline_media_uploader_script() { ?> @@ -688,6 +889,69 @@ public function inline_media_uploader_script() { ?> (function($) { + function renderMedia(uploader = null, attachmentIDs = []) { + const isMultiple = (uploader.getAttribute('data-media-uploader-multiple') === 'true'); + hideError(uploader); + hideButtons(uploader); + showLoader(uploader); + $.ajax({ + url: window.ajaxurl, + type: 'post', + data: { + action: 'wp_backstage_render_media', + attachment_ids: attachmentIDs, + is_multiple: isMultiple, + }, + success: function(result) { + const previewList = uploader.wpBackstage.ui.previewList; + hideLoader(uploader); + showButtons(uploader); + $(previewList).append($(result)); + if (isMultiple) { + initClones(uploader); + refreshSortable(uploader); + } else { + window.wp.mediaelement.initialize(); + } + }, + error: function() { + hideLoader(uploader); + showButtons(uploader); + showError(uploader); + }, + }); + } + + function showButtons(uploader = null) { + const buttons = uploader.wpBackstage.ui.buttons; + buttons.style.display = 'block'; + } + + function hideButtons(uploader = null) { + const buttons = uploader.wpBackstage.ui.buttons; + buttons.style.display = 'none'; + } + + function showLoader(uploader = null) { + const loader = uploader.wpBackstage.ui.loader; + loader.style.display = 'inline-block'; + } + + function hideLoader(uploader = null) { + const loader = uploader.wpBackstage.ui.loader; + loader.style.display = 'none'; + } + + function showError(uploader = null) { + const error = uploader.wpBackstage.ui.error; + error.style.display = 'inline-block'; + } + + function hideError(uploader = null) { + const error = uploader.wpBackstage.ui.error; + error.style.display = 'none'; + } + function findParentUploader(element = null) { var parentNode = element.parentNode; while (! parentNode.hasAttribute('data-media-uploader-id')) { @@ -704,11 +968,6 @@ function findParentAttachment(element = null) { return parentNode; } - function handleCloneClick(e = null) { - const uploader = findParentUploader(e.target); - uploader.wpBackstage.modal.open(); - } - function handleLegendClick(e = null) { const uploader = findParentUploader(e.target); uploader.wpBackstage.modal.open(); @@ -734,6 +993,14 @@ function handleRemoveButtonClick(e = null) { reset(uploader); } + function handleTryAgainButtonClick(e = null) { + const uploader = findParentUploader(e.target); + const attachmentIDs = getInputValue(uploader); + if (attachmentIDs.length > 0) { + appendClones(uploader, attachmentIDs, true); + } + } + function enableButton(button = null) { button.removeAttribute('disabled'); button.style.display = 'inline-block'; @@ -785,7 +1052,7 @@ function handleAttachmentRemoveButtonClick(e = null) { }); setInputValue(uploader, newAttachmentIDs); removeClone(uploader, attachmentID); - if (newAttachmentIDs.length <= 0) { + if (newAttachmentIDs.length < 1) { reset(uploader); } if (isMultiple) { @@ -793,42 +1060,35 @@ function handleAttachmentRemoveButtonClick(e = null) { } } - function getAttachmentURL(attachment = null, size = 'medium') { - let url = attachment.icon; - if (attachment.mime.includes('image')) { - url = (attachment.sizes && attachment.sizes[size]) ? attachment.sizes[size].url : attachment.url; - } - return url; + function initClones(uploader = null) { + const isMultiple = (uploader.getAttribute('data-media-uploader-multiple') === 'true'); + const previewList = uploader.wpBackstage.ui.previewList; + const clones = previewList.querySelectorAll('[data-attachment-id]') || []; + return Array.from(clones).map(function(clone) { + if (! clone.hasAttribute('data-wp-backstage-initialized')) { + const removeButton = clone.querySelector('.wp-backstage-media-uploader__attachment-remove'); + removeButton.addEventListener('click', handleAttachmentRemoveButtonClick); + clone.setAttribute('data-wp-backstage-initialized', true); + } + }); } - function appendClones(uploader = null, newAttachments = [], replace = false) { + function appendClones(uploader = null, attachmentIDs = [], replace = false) { const previewList = uploader.wpBackstage.ui.previewList; const template = uploader.wpBackstage.ui.template; const isMultiple = (uploader.getAttribute('data-media-uploader-multiple') === 'true'); const cloneIDs = getCloneIDs(uploader); const size = isMultiple ? 'thumbnail' : 'medium'; - const attachments = newAttachments.filter(function(attachment) { - return ! cloneIDs.includes(attachment.id); - }); if (replace) { removeClones(uploader); } - for (var i = 0; i < attachments.length; i++) { - const clone = template.cloneNode(true); - const image = clone.querySelector('img'); - const caption = clone.querySelector('.wp-backstage-media-uploader__attachment-caption'); - const removeButton = clone.querySelector('.wp-backstage-media-uploader__attachment-remove'); - const url = getAttachmentURL(attachments[i], size); - clone.setAttribute('data-attachment-id', attachments[i].id); - clone.style.cursor = isMultiple ? 'move' : 'pointer'; - image.setAttribute('src', url); - image.setAttribute('alt', attachments[i].alt); - image.setAttribute('title', attachments[i].title); - caption.setAttribute('title', attachments[i].caption); - removeButton.addEventListener('click', handleAttachmentRemoveButtonClick); - clone.addEventListener('click', handleCloneClick); - previewList.appendChild(clone); + if ( isMultiple ) { + attachmentIDs = attachmentIDs.filter(function(attachmentID) { + return ! cloneIDs.includes(attachmentID); + }); } + + renderMedia(uploader, attachmentIDs); } function removeClone(uploader = null, attachmentID = 0) { @@ -877,13 +1137,12 @@ function handleModalSelect(uploader = null) { }); const attachmentIDs = isMultiple ? concatAttachmentIDs(uploader, newAttachmentIDs) : newAttachmentIDs; setInputValue(uploader, attachmentIDs); - appendClones(uploader, attachments, ! isMultiple); + appendClones(uploader, attachmentIDs, ! isMultiple); disableButton(addButton); enableButton(removeButton); if (isMultiple) { enableButton(addToButton); disableButton(replaceButton); - refreshSortable(uploader); } else { disableButton(addToButton); enableButton(replaceButton); @@ -895,11 +1154,16 @@ function destroy(uploader = null) { const addToButton = uploader.wpBackstage.ui.addToButton; const replaceButton = uploader.wpBackstage.ui.replaceButton; const removeButton = uploader.wpBackstage.ui.removeButton; + const tryAgainButton = uploader.wpBackstage.ui.tryAgainButton; const legend = uploader.wpBackstage.ui.legend; addButton.removeEventListener('click', handleAddButtonClick); addToButton.removeEventListener('click', handleAddToButtonClick); replaceButton.removeEventListener('click', handleReplaceButtonClick); removeButton.removeEventListener('click', handleRemoveButtonClick); + tryAgainButton.removeEventListener('click', handleTryAgainButtonClick); + hideLoader(uploader); + hideError(uploader); + showButtons(uploader); removeClones(uploader); if (legend) { legend.removeEventListener('click', handleLegendClick); @@ -921,22 +1185,7 @@ function initPreview(uploader = null) { const isMultiple = (uploader.getAttribute('data-media-uploader-multiple') === 'true'); const attachmentIDs = getInputValue(uploader); if (attachmentIDs.length > 0) { - const query = wp.media.query({ - order: 'ASC', - orderby: 'post__in', - perPage: -1, - post__in: attachmentIDs, - query: true, - }); - query.more().done(function() { - const attachments = this.models.map(function(model) { - return model.attributes; - }); - appendClones(uploader, attachments); - if (isMultiple) { - refreshSortable(uploader); - } - }); + appendClones(uploader, attachmentIDs); } if (isMultiple) { initSortable(uploader); @@ -947,13 +1196,16 @@ function init(uploader = null) { const fieldId = uploader.getAttribute('data-media-uploader-id'); const input = uploader.querySelector('#' + fieldId); const legend = uploader.querySelector('#' + fieldId + '_legend'); + const buttons = uploader.querySelector('#' + fieldId + '_buttons'); const addButton = uploader.querySelector('#' + fieldId + '_button_add'); const addToButton = uploader.querySelector('#' + fieldId + '_button_add_to'); const replaceButton = uploader.querySelector('#' + fieldId + '_button_replace'); const removeButton = uploader.querySelector('#' + fieldId + '_button_remove'); const preview = uploader.querySelector('#' + fieldId + '_preview'); - const previewList = preview.querySelector('#' + fieldId + '_preview_list'); - const template = preview.querySelector('[data-attachment-id="0"]'); + const previewList = uploader.querySelector('#' + fieldId + '_preview_list'); + const loader = uploader.querySelector('#' + fieldId + '_loader'); + const error = uploader.querySelector('#' + fieldId + '_error'); + const tryAgainButton = uploader.querySelector('#' + fieldId + '_button_try_again'); const title = uploader.getAttribute('data-media-uploader-title'); const buttonText = uploader.getAttribute('data-media-uploader-button'); const type = uploader.getAttribute('data-media-uploader-type'); @@ -972,6 +1224,7 @@ function init(uploader = null) { addToButton.addEventListener('click', handleAddToButtonClick); replaceButton.addEventListener('click', handleReplaceButtonClick); removeButton.addEventListener('click', handleRemoveButtonClick); + tryAgainButton.addEventListener('click', handleTryAgainButtonClick); if (legend) { legend.addEventListener('click', handleLegendClick); } @@ -980,13 +1233,16 @@ function init(uploader = null) { ui: { input: input, legend: legend, + buttons: buttons, addButton: addButton, addToButton: addToButton, replaceButton: replaceButton, removeButton: removeButton, preview: preview, previewList: previewList, - template: template, + loader: loader, + error: error, + tryAgainButton: tryAgainButton, }, }; initPreview(uploader); @@ -1022,6 +1278,9 @@ function reset(uploader = null) { const addToButton = uploader.wpBackstage.ui.addToButton; const replaceButton = uploader.wpBackstage.ui.replaceButton; const removeButton = uploader.wpBackstage.ui.removeButton; + hideLoader(uploader); + hideError(uploader); + showButtons(uploader); enableButton(addButton); disableButton(addToButton); disableButton(replaceButton); @@ -1881,17 +2140,18 @@ public function inline_nav_menu_item_customizer_script() { ?> var controlExpandTimer = null; - function setControlElementValue(controlElement = null, value = undefined) { + function setControlElementValue(controlElement = null, value = undefined, instanceNumber = 0) { const fieldName = controlElement.element.attr('data-wp-backstage-field-name'); const fieldType = controlElement.element.attr('data-wp-backstage-field-type'); + const elementName = 'menu-item-' + fieldName + '[' + instanceNumber + ']'; switch (fieldType) { case 'checkbox': { - const input = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const input = controlElement.element.find('[name="' + elementName + '"]'); input.attr('checked', Boolean(value)); break; } case 'radio': { - const radios = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const radios = controlElement.element.find('[name="' + elementName + '"]'); if (! value && (radios.length > 0)) { value = $(radios[0]).val(); } @@ -1902,7 +2162,7 @@ function setControlElementValue(controlElement = null, value = undefined) { break; } case 'checkbox_set': { - const checkboxes = controlElement.element.find('[name="menu-item-' + fieldName + '[]"]'); + const checkboxes = controlElement.element.find('[name="' + elementName + '[]"]'); checkboxes.each(function() { const checkbox = $(this); checkbox.attr('checked', (Array.isArray(value) && value.includes(checkbox.val()))); @@ -1911,22 +2171,22 @@ function setControlElementValue(controlElement = null, value = undefined) { } case 'time': { const timePieces = (value && ! Array.isArray(value)) ? value.split(':') : value; - const hourSelect = controlElement.element.find('[name="menu-item-' + fieldName + '[hour]"]'); - const minuteSelect = controlElement.element.find('[name="menu-item-' + fieldName + '[minute]"]'); - const secondSelect = controlElement.element.find('[name="menu-item-' + fieldName + '[second]"]'); + const hourSelect = controlElement.element.find('[name="' + elementName + '[hour]"]'); + const minuteSelect = controlElement.element.find('[name="' + elementName + '[minute]"]'); + const secondSelect = controlElement.element.find('[name="' + elementName + '[second]"]'); hourSelect.val((timePieces && timePieces[0]) ? timePieces[0] : hourSelect.find('option:first-child').val()); minuteSelect.val((timePieces && timePieces[1]) ? timePieces[1] : minuteSelect.find('option:first-child').val()); secondSelect.val((timePieces && timePieces[2]) ? timePieces[2] : secondSelect.find('option:first-child').val()); break; } case 'address': { - const countrySelect = controlElement.element.find('[name="menu-item-' + fieldName + '[country]"]'); - const address1Input = controlElement.element.find('[name="menu-item-' + fieldName + '[address_1]"]'); - const address2Input = controlElement.element.find('[name="menu-item-' + fieldName + '[address_2]"]'); - const cityInput = controlElement.element.find('[name="menu-item-' + fieldName + '[city]"]'); - const stateSelect = controlElement.element.find('select[name="menu-item-' + fieldName + '[state]"]'); - const stateInput = controlElement.element.find('input[name="menu-item-' + fieldName + '[state]"]'); - const zipInput = controlElement.element.find('[name="menu-item-' + fieldName + '[zip]"]'); + const countrySelect = controlElement.element.find('[name="' + elementName + '[country]"]'); + const address1Input = controlElement.element.find('[name="' + elementName + '[address_1]"]'); + const address2Input = controlElement.element.find('[name="' + elementName + '[address_2]"]'); + const cityInput = controlElement.element.find('[name="' + elementName + '[city]"]'); + const stateSelect = controlElement.element.find('select[name="' + elementName + '[state]"]'); + const stateInput = controlElement.element.find('input[name="' + elementName + '[state]"]'); + const zipInput = controlElement.element.find('[name="' + elementName + '[zip]"]'); countrySelect.val((value && value.country) ? value.country : 'US'); address1Input.val((value && value.address_1) ? value.address_1 : ''); address2Input.val((value && value.address_2) ? value.address_2 : ''); @@ -1937,41 +2197,46 @@ function setControlElementValue(controlElement = null, value = undefined) { break; } case 'select': { - const select = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const select = controlElement.element.find('[name="' + elementName + '"]'); select.val(value || select.find('option:first-child').val()); break; } case 'select_posts': { - const select = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); - select.val(value.toString() || select.find('option:first-child').val()); + const select = controlElement.element.find('[name="' + elementName + '"]'); + value = parseInt(value, 10); + value = value ? value.toString() : undefined; + select.val(value || select.find('option:first-child').val()); break; } case 'select_users': { - const select = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); - select.val(value.toString() || select.find('option:first-child').val()); + const select = controlElement.element.find('[name="' + elementName + '"]'); + value = parseInt(value, 10); + value = value ? value.toString() : undefined; + select.val(value || select.find('option:first-child').val()); break; } default: { - const input = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const input = controlElement.element.find('[name="' + elementName + '"]'); input.val(value); break; } } } - function initControlElementChangeHandler(controlElement = null, setting = null) { + function initControlElementChangeHandler(controlElement = null, setting = null, instanceNumber = 0) { const fieldName = controlElement.element.attr('data-wp-backstage-field-name'); const fieldType = controlElement.element.attr('data-wp-backstage-field-type'); + const elementName = 'menu-item-' + fieldName + '[' + instanceNumber + ']'; switch (fieldType) { case 'checkbox': { - const input = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const input = controlElement.element.find('[name="' + elementName + '"]'); input.on('change', function(e) { handleSettingChange(setting, fieldName, e.target.checked ? e.target.value : undefined); }); break; } case 'radio': { - const radios = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const radios = controlElement.element.find('[name="' + elementName + '"]'); radios.each(function() { const radio = $(this); radio.on('change', function(e) { @@ -1983,7 +2248,7 @@ function initControlElementChangeHandler(controlElement = null, setting = null) break; } case 'checkbox_set': { - const checkboxes = controlElement.element.find('[name="menu-item-' + fieldName + '[]"]'); + const checkboxes = controlElement.element.find('[name="' + elementName + '[]"]'); checkboxes.each(function() { const checkbox = $(this); checkbox.on('change', function(e) { @@ -1993,9 +2258,9 @@ function initControlElementChangeHandler(controlElement = null, setting = null) break; } case 'time': { - const hourSelect = controlElement.element.find('[name="menu-item-' + fieldName + '[hour]"]'); - const minuteSelect = controlElement.element.find('[name="menu-item-' + fieldName + '[minute]"]'); - const secondSelect = controlElement.element.find('[name="menu-item-' + fieldName + '[second]"]'); + const hourSelect = controlElement.element.find('[name="' + elementName + '[hour]"]'); + const minuteSelect = controlElement.element.find('[name="' + elementName + '[minute]"]'); + const secondSelect = controlElement.element.find('[name="' + elementName + '[second]"]'); hourSelect.on('change', function(e) { handleTimeSettingChange(setting, fieldName, 0, e.target.value); }); @@ -2008,13 +2273,13 @@ function initControlElementChangeHandler(controlElement = null, setting = null) break; } case 'address': { - const countrySelect = controlElement.element.find('[name="menu-item-' + fieldName + '[country]"]'); - const address1Input = controlElement.element.find('[name="menu-item-' + fieldName + '[address_1]"]'); - const address2Input = controlElement.element.find('[name="menu-item-' + fieldName + '[address_2]"]'); - const cityInput = controlElement.element.find('[name="menu-item-' + fieldName + '[city]"]'); - const stateSelect = controlElement.element.find('select[name="menu-item-' + fieldName + '[state]"]'); - const stateInput = controlElement.element.find('input[name="menu-item-' + fieldName + '[state]"]'); - const zipInput = controlElement.element.find('[name="menu-item-' + fieldName + '[zip]"]'); + const countrySelect = controlElement.element.find('[name="' + elementName + '[country]"]'); + const address1Input = controlElement.element.find('[name="' + elementName + '[address_1]"]'); + const address2Input = controlElement.element.find('[name="' + elementName + '[address_2]"]'); + const cityInput = controlElement.element.find('[name="' + elementName + '[city]"]'); + const stateSelect = controlElement.element.find('select[name="' + elementName + '[state]"]'); + const stateInput = controlElement.element.find('input[name="' + elementName + '[state]"]'); + const zipInput = controlElement.element.find('[name="' + elementName + '[zip]"]'); countrySelect.on('change', function(e) { handleAddressSettingChange(setting, fieldName, 'country', e.target.value); }); @@ -2039,28 +2304,28 @@ function initControlElementChangeHandler(controlElement = null, setting = null) break; } case 'select': { - const select = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const select = controlElement.element.find('[name="' + elementName + '"]'); select.on('change', function(e) { handleSettingChange(setting, fieldName, e.target.value ); }); break; } case 'select_posts': { - const select = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const select = controlElement.element.find('[name="' + elementName + '"]'); select.on('change', function(e) { handleSettingChange(setting, fieldName, e.target.value ); }); break; } case 'select_users': { - const select = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const select = controlElement.element.find('[name="' + elementName + '"]'); select.on('change', function(e) { handleSettingChange(setting, fieldName, e.target.value ); }); break; } default: { - const input = controlElement.element.find('[name="menu-item-' + fieldName + '"]'); + const input = controlElement.element.find('[name="' + elementName + '"]'); input.on('change input propertychange paste', function(e) { handleSettingChange(setting, fieldName, e.target.value); }); @@ -2073,7 +2338,7 @@ function setControlValues(control = null) { const values = control.setting(); const elements = control.wpBackstageElements; for (var fieldName in elements) { - setControlElementValue(elements[fieldName], values[fieldName]); + setControlElementValue(elements[fieldName], values[fieldName], control.params.instanceNumber); } } @@ -2115,7 +2380,6 @@ function handleSectionInitSortables(section = null) { } function handleSettingChange(setting = null, fieldName = '', value = undefined) { - console.log(value); const currentValues = setting(); if (currentValues[fieldName] !== value) { setting.set(Object.assign( @@ -2163,7 +2427,7 @@ function handleCheckboxSetSettingChange(setting = null, fieldName = '', value = function initChangeHandlers(control = null) { const elements = control.wpBackstageElements; for (var fieldName in elements) { - initControlElementChangeHandler(elements[fieldName], control.setting); + initControlElementChangeHandler(elements[fieldName], control.setting, control.params.instanceNumber); } } @@ -2550,4 +2814,161 @@ function init() { + + + + + + $attachment['filename'], + ) + ); ?> + + + + + + + + + esc_url( $attachment['url'] ), + 'preload' => 'metadata', + 'class' => 'wp-video-shortcode wp-mediaelement-keep', + ), + ); ?> + + + + + + esc_url( $attachment['url'] ), + 'preload' => 'metadata', + 'class' => 'wp-audio-shortcode wp-mediaelement-keep', + ), + ); ?> + + + + + + + $attachment['filename'], + ) + ); ?> + + + + + + + + $attachment['filename'], + ) + ); ?> + + + + + + + + + + + render_media_item( $attachment_id, $is_multiple ); + } + + wp_die(); + } + } diff --git a/languages/wp-backstage.pot b/languages/wp-backstage.pot index bba19a7..1b18eef 100644 --- a/languages/wp-backstage.pot +++ b/languages/wp-backstage.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2022-10-31T14:32:50+00:00\n" +"POT-Creation-Date: 2022-11-09T16:52:34+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.6.0\n" "X-Domain: wp_backstage\n" @@ -1334,93 +1334,103 @@ msgid "Filter by %1$s" msgstr "" #. translators: 1: field label -#: includes/class-wp-backstage-component.php:2863 +#: includes/class-wp-backstage-component.php:2864 msgctxt "media uploader field - modal single button" msgid "Set %1$s" msgstr "" #. translators: 1: field label -#: includes/class-wp-backstage-component.php:2868 +#: includes/class-wp-backstage-component.php:2869 msgctxt "media uploader field - modal multiple button" msgid "Add to %1$s" msgstr "" #. translators: 1: field label -#: includes/class-wp-backstage-component.php:2873 +#: includes/class-wp-backstage-component.php:2874 msgctxt "media uploader field - add button" msgid "Add %1$s" msgstr "" #. translators: 1: field label -#: includes/class-wp-backstage-component.php:2878 +#: includes/class-wp-backstage-component.php:2879 msgctxt "media uploader field - add to button" msgid "Add to %1$s" msgstr "" #. translators: 1: field label -#: includes/class-wp-backstage-component.php:2883 +#: includes/class-wp-backstage-component.php:2884 msgctxt "media uploader field - replace button" msgid "Replace %1$s" msgstr "" #. translators: 1: field label -#: includes/class-wp-backstage-component.php:2888 +#: includes/class-wp-backstage-component.php:2889 msgctxt "media uploader field - remove button" msgid "Remove %1$s" msgstr "" -#: includes/class-wp-backstage-component.php:2930 -msgctxt "media uploader field - remove" -msgid "Remove" +#: includes/class-wp-backstage-component.php:2892 +msgctxt "media uploader field - loader" +msgid "Loading preview..." +msgstr "" + +#: includes/class-wp-backstage-component.php:2893 +msgctxt "media uploader field - error" +msgid "There was an error rendering the preview." +msgstr "" + +#: includes/class-wp-backstage-component.php:2894 +msgctxt "media uploader field - try again button" +msgid "Try again" msgstr "" -#: includes/class-wp-backstage-component.php:3059 +#: includes/class-wp-backstage-component.php:3075 msgctxt "address field - country label" msgid "Country" msgstr "" -#: includes/class-wp-backstage-component.php:3103 +#: includes/class-wp-backstage-component.php:3119 msgctxt "address field - line 1" msgid "Address" msgstr "" -#: includes/class-wp-backstage-component.php:3136 +#: includes/class-wp-backstage-component.php:3152 msgctxt "address field - line 2" msgid "Address (Line 2)" msgstr "" -#: includes/class-wp-backstage-component.php:3169 +#: includes/class-wp-backstage-component.php:3185 msgctxt "address field - city" msgid "City" msgstr "" -#: includes/class-wp-backstage-component.php:3202 +#: includes/class-wp-backstage-component.php:3218 msgctxt "address field - state input" msgid "State / Province / Region" msgstr "" -#: includes/class-wp-backstage-component.php:3234 +#: includes/class-wp-backstage-component.php:3250 msgctxt "address field - state select" msgid "State" msgstr "" -#: includes/class-wp-backstage-component.php:3278 +#: includes/class-wp-backstage-component.php:3294 msgctxt "address field - zip" msgid "Zip Code" msgstr "" -#: includes/class-wp-backstage-component.php:3344 +#: includes/class-wp-backstage-component.php:3360 msgctxt "select posts field - default option none label" msgid "Select" msgstr "" -#: includes/class-wp-backstage-component.php:3443 +#: includes/class-wp-backstage-component.php:3459 msgctxt "select users field - default option none label" msgid "Select" msgstr "" #. translators: 1: user display name, 2: user username -#: includes/class-wp-backstage-component.php:3490 +#: includes/class-wp-backstage-component.php:3506 msgctxt "select users field - option label" msgid "%1$s (%2$s)" msgstr "" @@ -1851,48 +1861,53 @@ msgid "Classic Widgets" msgstr "" #. translators: 1: plugin name link. -#: includes/class-wp-backstage.php:212 +#: includes/class-wp-backstage.php:214 msgctxt "plugin dependency message" msgid "[WP Backstage Plugin Dependency] The %1$s plugin must be installed and activated." msgstr "" #. translators: 1: error message. -#: includes/class-wp-backstage.php:279 +#: includes/class-wp-backstage.php:281 msgctxt "plugin error message" msgid "Error: %1$s" msgstr "" -#: includes/class-wp-backstage.php:311 +#: includes/class-wp-backstage.php:313 msgctxt "help tab - debug title" msgid "Debug" msgstr "" -#: includes/class-wp-backstage.php:312 +#: includes/class-wp-backstage.php:314 msgctxt "help tab - debug description" msgid "The following is useful debug information for WP Backstage development." msgstr "" -#: includes/class-wp-backstage.php:313 +#: includes/class-wp-backstage.php:315 msgctxt "help tab - debug current screen" msgid "Current Screen" msgstr "" -#: includes/class-wp-backstage.php:316 +#: includes/class-wp-backstage.php:318 msgctxt "help tab - debug current screen parent base label" msgid "Parent Base:" msgstr "" -#: includes/class-wp-backstage.php:321 +#: includes/class-wp-backstage.php:323 msgctxt "help tab - debug current screen base" msgid "Base:" msgstr "" -#: includes/class-wp-backstage.php:326 +#: includes/class-wp-backstage.php:328 msgctxt "help tab - debug current screen id" msgid "ID:" msgstr "" -#: includes/class-wp-backstage.php:348 +#: includes/class-wp-backstage.php:350 msgctxt "help tab title" msgid "WP Backstage" msgstr "" + +#: includes/class-wp-backstage.php:2835 +msgctxt "media item - remove" +msgid "Remove" +msgstr ""