self::panel_label(), 'target' => 'prodtabs_product_data', 'class' => array(), 'priority' => 75, ); return $tabs; } /** Scripts and styles of the product screen. */ public static function assets(): void { $screen = get_current_screen(); if ( ! $screen || 'product' !== $screen->id ) { return; } wp_enqueue_editor(); wp_enqueue_style( 'prodtabs-admin', PRODTABS_URL . 'assets/css/admin.css', array(), Plugin::asset_version( 'assets/css/admin.css' ) ); $deps = array( 'jquery', 'jquery-ui-sortable', 'wp-a11y' ); if ( wp_script_is( 'selectWoo', 'registered' ) ) { $deps[] = 'selectWoo'; wp_enqueue_style( 'select2' ); } wp_enqueue_script( 'prodtabs-admin-product', PRODTABS_URL . 'assets/js/admin-product.js', $deps, Plugin::asset_version( 'assets/js/admin-product.js' ), true ); Display::copy_script(); wp_localize_script( 'prodtabs-admin-product', 'prodtabsProduct', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE_AJAX ), 'productId' => (int) get_the_ID(), 'i18n' => array( /* translators: %s: tab title. */ 'moveUp' => __( 'Move “%s” up', 'prodtabs' ), /* translators: %s: tab title. */ 'moveDown' => __( 'Move “%s” down', 'prodtabs' ), /* translators: %s: tab title. */ 'remove' => __( 'Remove “%s”', 'prodtabs' ), /* translators: 1: tab title, 2: new position, 3: number of tabs. */ 'moved' => __( '“%1$s” moved to position %2$d of %3$d.', 'prodtabs' ), /* translators: %s: tab title. */ 'added' => __( '“%s” added.', 'prodtabs' ), /* translators: %s: tab title. */ 'removed' => __( '“%s” removed.', 'prodtabs' ), /* translators: %s: tab title. */ 'confirmRemove' => __( 'Remove the tab “%s”? Its content will be lost when you update the product.', 'prodtabs' ), 'confirmRevert' => __( 'Go back to the shared version? The changes made to this tab on this product will be lost when you update the product.', 'prodtabs' ), 'untitled' => __( '(no title)', 'prodtabs' ), 'error' => __( 'Something went wrong. Reload the page and try again.', 'prodtabs' ), 'newTab' => __( 'New tab', 'prodtabs' ), ), ) ); } /** Panel content. */ public static function panel(): void { global $post; $product_id = $post ? (int) $post->ID : 0; $entries = $product_id ? Product_Tabs::get( $product_id ) : array(); $product = $product_id ? wc_get_product( $product_id ) : null; if ( $product ) { $entries = Product_Tabs::with_auto( $entries, $product ); } if ( ! $entries ) { $entries = Tabs::with_natives( array(), Settings::get( 'native_order' ) ); } $context = array( 'lang' => $product_id ? Multilingual::post_language( $product_id ) : '', 'locked' => $product_id && Multilingual::structure_locked( $product_id ), 'source' => self::source_texts( $product_id ), 'product' => $product_id, ); $library = $context['locked'] ? array() : self::library_choices( $context['lang'] ); ?>
Library::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 500, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, ); if ( '' !== $lang ) { $args['lang'] = $lang; // Polylang. $args['suppress_filters'] = false; // WPML filters the query by the current admin language. } $choices = array(); foreach ( get_posts( $args ) as $post ) { $shop = Library::shop_title( $post ); /* translators: 1: title of the tab in the shop, 2: name of the library tab. */ $choices[ $post->ID ] = $shop === $post->post_title ? $shop : sprintf( __( '%1$s (%2$s)', 'prodtabs' ), $shop, $post->post_title ); } return $choices; } /** * One row of the list. * * @param array $entry Normalised entry. * @param int|string $index Position in the form (a placeholder in the template). * @param array $context lang, locked, source (texts of the source product). * @param bool $expanded Whether the row opens on display. */ public static function row( array $entry, $index, array $context = array(), bool $expanded = false ): string { $context = wp_parse_args( $context, array( 'lang' => '', 'locked' => false, 'source' => array(), 'product' => 0, ) ); $index = (string) $index; $name = 'prodtabs[entries][' . $index . ']'; $type = $entry['type']; $title = self::row_title( $entry, $context ); $body = ''; $notes = array(); $locked = (bool) $context['locked']; if ( 'native' === $type ) { $natives = Settings::get( 'natives' ); $visible = null === $entry['visible'] ? $natives[ $entry['key'] ]['visible'] : $entry['visible']; $value = null === $entry['visible'] ? '' : ( $entry['visible'] ? '1' : '0' ); $badge = __( 'WooCommerce', 'prodtabs' ); $body = self::native_body( $entry, $context ); } else { $visible = (bool) $entry['visible']; $value = $visible ? '1' : '0'; $badge = 'local' === $type ? __( 'This product', 'prodtabs' ) : __( 'Shared', 'prodtabs' ); $body = 'local' === $type ? self::local_body( $entry, $index, $name, $context ) : self::library_body( $entry, $context ); if ( 'library' === $type && ! Library::get_tab( (int) $entry['library_id'] ) ) { $notes[] = __( 'Not published: not shown in the shop.', 'prodtabs' ); } if ( ! empty( $entry['auto'] ) ) { $notes[] = __( 'Added automatically', 'prodtabs' ); } if ( 'local' === $type && empty( $entry['translated'] ) ) { $notes[] = __( 'Not translated', 'prodtabs' ); } } $title_id = 'prodtabs-title-' . $index; $body_id = 'prodtabs-body-' . $index; $classes = array( 'prodtabs-item', 'prodtabs-item--' . $type ); if ( ! $visible ) { $classes[] = 'is-hidden'; } if ( $expanded ) { $classes[] = 'is-open'; } ob_start(); ?>>
post_type ) : ?>
get_id(), $entry['key'] ), __( 'Shortcode of this tab', 'prodtabs' ), 'prodtabs-shortcode-' . $entry['key'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in shortcode_field(). ?> get_id() ) ) { return; } $product_id = $product->get_id(); // The last field of the panel is missing when PHP dropped fields (max_input_vars): saving would lose tabs. if ( empty( $_POST['prodtabs']['complete'] ) ) { self::notice( __( 'The tabs of this product were not saved: the form was too long for the server (max_input_vars setting of PHP). Your host can raise this limit.', 'prodtabs' ) ); return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- cleaned by normalize() and sanitize_input(). $raw = isset( $_POST['prodtabs']['entries'] ) && is_array( $_POST['prodtabs']['entries'] ) ? wp_unslash( $_POST['prodtabs']['entries'] ) : array(); $entries = Product_Tabs::sanitize_input( Tabs::normalize( array_values( $raw ) ) ); // Structure changed in another language while this screen was open (Polylang shares it): keep the stored one. $loaded = isset( $_POST['prodtabs']['signature'] ) ? sanitize_key( wp_unslash( $_POST['prodtabs']['signature'] ) ) : ''; $stored_now = Tabs::signature( Product_Tabs::layout( $product_id ) ); $texts_only = Multilingual::structure_locked( $product_id ); if ( '' !== $loaded && $loaded !== $stored_now ) { $texts_only = true; if ( Tabs::signature( $entries ) !== $loaded ) { self::notice( __( 'The list of tabs of this product was changed in another language while you were editing it. The texts you typed were saved, but not your changes to the list: check the Tabs panel and make them again.', 'prodtabs' ) ); } } // A tab added and left empty is dropped; a stored tab without text stays, to be translated later. $stored = array(); foreach ( Product_Tabs::layout( $product_id ) as $item ) { if ( is_array( $item ) && isset( $item['key'] ) ) { $stored[ (string) $item['key'] ] = true; } } $entries = array_values( array_filter( $entries, static function ( $entry ) use ( $stored ) { return 'local' !== $entry['type'] || isset( $stored[ $entry['key'] ] ) || '' !== $entry['title'] || '' !== trim( $entry['content'] ); } ) ); // New tabs get a readable key from their title, the name translators see in WPML. Keys still holding texts in // another language (a tab removed earlier) are avoided: those old texts would come back as translations. $keys = array(); foreach ( Multilingual::translation_ids( $product_id, 'product' ) as $translation ) { $texts = get_post_meta( $translation, Tabs::META_TEXTS, true ); $keys += is_array( $texts ) ? array_fill_keys( array_map( 'strval', array_keys( $texts ) ), true ) : array(); } foreach ( $entries as $entry ) { if ( 'local' === $entry['type'] && isset( $stored[ $entry['key'] ] ) ) { $keys[ $entry['key'] ] = true; } } foreach ( $entries as $i => $entry ) { if ( 'local' === $entry['type'] && ! isset( $stored[ $entry['key'] ] ) ) { $entries[ $i ]['key'] = Tabs::key_from( $entry['title'], $keys ); $keys[ $entries[ $i ]['key'] ] = true; } } Product_Tabs::set( $product, $entries, $texts_only ); } /** * Message shown on the product screen after the redirection that follows saving. * * @param string $message Message. */ private static function notice( string $message ): void { set_transient( 'prodtabs_notice_' . get_current_user_id(), $message, 5 * MINUTE_IN_SECONDS ); } /** Shows the message left by the last save. */ public static function show_notice(): void { $key = 'prodtabs_notice_' . get_current_user_id(); $message = get_transient( $key ); if ( ! $message ) { return; } delete_transient( $key ); echo '
' . esc_html( (string) $message ) . '