Добавляем Media Uploader для custom fields или еще один featured image

Все началось где-то год назад с того что я хотел добавить медиа загрузчик для плагина menu image, но увы в то время без дублирования js файла из коры wordpress ничего нельзя было сделать, а копировать код не хотел по причине того что его нужно больше поддерживать при обновлении, на том и забил. Но недавно решил пересмотреть что изменилось, и вот что из этого получилось.

Статья может показаться большой, но это только из-за обилия кода и мучительных разъяснений, но я разобью ее по разделам чтобы потом можно было вернутся и дочитать.

Но прежде всего нужно создать тему (можно и плагин) в которой будет весь этот код. За основу я взял стандартную twentyfourteen, хотя это и не имеет никакого значение, главное чтобы wordpress был свежий.
Создаем папку с названием twentyfourteen-child внутри wp-content/themes и добавляем в эту папку файл style.css с вот таким содержанием:

/*
 Theme Name:   Twentyfourteen Child Theme
 Template:     twentyfourteen
*/
@import url("../twentyfourteen/style.css");

Теперь заходим в админку и включаем новую тему, внешне она ничем не будет отличаться от twentyfourteen. Затем создаем functions.php и читаем дальше.

Добавляем metabox с выводом html код нашей картинки

Регистрируем функцию на событие admin_init:

/**
 * Action on admin init.
 *
 * Place here register post types, taxonomies or meta boxes.
 */
function child_theme_admin_init_action() {
    // meta box for post
    add_meta_box(
        'alternative_image', // meta box #id
        __('Alternative image', 'child_theme'), // meta box name
        'child_theme_meta_box_callback', // callback function
        'post', // screen: post or page or any other custom post type
        'side' // position: site, advanced
    );
}

add_action('admin_init', 'child_theme_admin_init_action');

Сейчас мы только добавили metabox и ничего не должно работать потому что callback функции для вызов еще нет, добавляем ее:

/**
 * Meta box callback.
 *
 * @param $post WP_Post
 */
function child_theme_meta_box_callback($post) {
        echo 'Metabox html content';
}

Теперь можно смотреть, и вот что должно получится Metabox with html code

Теперь добавим более полезный html код внутрь metabox’а, добавим вот такую функцию:

/**
 * Show alternative image, if false show link to add it.
 *
 * @param null $thumb_id
 * @return string html code
 */
function child_theme_get_alt_thumbnail_html( $thumb_id = null ) {
    global $content_width, $_wp_additional_image_sizes;

    $set_thumbnail_link = '<p class="hide-if-no-js"><a title="%s" href="#" id="set-post-alternative-thumbnail">%s</a></p>';
    $content            = sprintf( $set_thumbnail_link, esc_attr__( 'Set alternative image', 'dev' ), esc_html__( 'Set alternative image' ) );

    if ( $thumb_id && get_post( $thumb_id ) ) {
        $old_content_width = $content_width;
        $content_width     = 266;
        if ( !isset( $_wp_additional_image_sizes['post-thumbnail'] ) ) {
            $thumbnail_html = wp_get_attachment_image( $thumb_id, array( $content_width, $content_width ) );
        } else {
            $thumbnail_html = wp_get_attachment_image( $thumb_id, 'post-thumbnail' );
        }
        if ( !empty( $thumbnail_html ) ) {
            $content    = sprintf( $set_thumbnail_link, esc_attr__( 'Set alternative image', 'dev' ), $thumbnail_html );
            $content .= '<p class="hide-if-no-js"><a href="#" id="remove-post-alternative-thumbnail">' . esc_html__( 'Remove alternative image' ) . '</a></p>';
        }
        $content_width = $old_content_width;
    }

    return $content;
}

Функция работает просто: принимает параметром id изображения и выводит его, иначе выводит обычную ссылку при нажатии на которую мы будем вызывать медиа загрузчик. Вызываем эту функцию в нашем metabox’е:

/**
 * Meta box callback.
 *
 * @param $post WP_Post
 */
function child_theme_meta_box_callback($post) {
    $alt_image_id = get_post_meta( $post->ID, '_alt_thumbnail_id', true );
    echo child_theme_get_alt_thumbnail_html( $alt_image_id );
}

Тут тоже просто, берем у поста meta информацию по ключу _alt_thumbnail_id и передаем в функцию которая выводит html. Мета данных с таким ключом еще нет, так что в админке увидим сейчас только нерабочую ссылку. Metabox with link to add attachment

На этом первая часть завешена, далее будет js.

Подключаем js файл который будет вызывать медиа загрузчик

Сначала создадим в корне темы директорию js внутри которой поместим файл alternative_image.admin.js. Добавляем функцию на событие admin_enqueue_scripts внутри которой и подключим наш файл и парочку файлов из коры:

/**
 * Enqueue admin scripts.
 *
 * @param $hook string
 */
function child_theme_admin_enqueue_scripts_action( $hook ) {
    global $post;

    // check if it's a post edit page and not any other admin page
    if (
        ( $hook == 'post-new.php' || $hook == 'post.php' )
        && $post->post_type == 'post'
    ) {

        // load media library scripts
        wp_enqueue_media();
        wp_enqueue_style( 'editor-buttons' );

        // load our js file
        wp_enqueue_script(
            'alternative-image',
            get_stylesheet_directory_uri() . '/js/alternative_image.admin.js',
            array( 'jquery' )
        );

        // add variables to our js file
        wp_localize_script(
            'alternative-image',
            'child_theme',
            array(
                'l10n'     => array(
                    'uploaderTitle' => __( 'Set alternative image', 'child_theme' ),
                    'uploaderButton' => __( 'Select image', 'child_theme' ),
                ),
                'nonce' => wp_create_nonce( 'set_post_alternative_thumbnail_' . $post->ID ),
            )
        );
    }
}

add_action('admin_enqueue_scripts', 'child_theme_admin_enqueue_scripts_action');

В этой функции wp_localize_script может показать не знакомой функцией, но она просто передает параметры из php в js. Из самих параметров есть массив с переводами, это заголовок окна медиа загрузчика и кнопка при выборе картинки, и параметр nonce, в который попадет уникальный код который будет отправляться с ajax запросом и который нужен для предотвращения csrf.
А в js файле будет вот такой код:

(function ($) {
    $(document ).ready(function() {
        var AltImageUpdate = function ( post_id, thumb_id ) {
            wp.media.post( 'set_alternative_thumbnail', {
                post_id:      post_id,
                thumbnail_id: thumb_id,
                nonce:        child_theme.nonce
            } ).done( function ( html ) {
                $('#alternative_image' ).find('.inside' ).html(html);
            } );
        };

        $('#alternative_image' )
            .on('click', '#set-post-alternative-thumbnail', function(e) {
                e.preventDefault();
                e.stopPropagation();

                var uploader = wp.media( {
                    title:    child_theme.l10n.uploaderTitle,
                    button:   { text: child_theme.l10n.uploaderButton },
                    multiple: false
                } );
                uploader.on('select', function() {
                    var attachment = uploader.state().get( 'selection' ).first().toJSON();
                    AltImageUpdate( wp.media.view.settings.post.id, attachment.id );
                });
                uploader.open();
            })
            .on('click', '#remove-post-alternative-thumbnail', function(e) {
                e.preventDefault();
                e.stopPropagation();

                AltImageUpdate( wp.media.view.settings.post.id, -1 );
            });
    });
})(jQuery);

В нем тоже нет ничего страшного, все «легко и просто»™. Id нашего metabox’а — alternative_image, по-этому привязываемся на клики внутри него. Клика будет два: добавление изображения и удаление. При клике на добавления изображения вызываем медиа загрузчик с параметрами которые передавали через wp_localize_script. После выбора изображения и нажатия на кнопку, загрузчик вызовет событие select на которое у нас добавлена функция которая первую выбранную картинку (а их может быть несколько, см. параметр multiple) и вызывает функцию AltImageUpdate с параметром id поста и id выбранного изображения. А при клике на удаление изображения вызывается та же функция но вместо id изображения передаем -1. Сама же функция AltImageUpdate просто отправляет ajax запрос, которым мы потом обработаем, и полученные данные вставляет в наш metabox.

Сейчас самое время посмотреть в админку на результат. При клике на ссылку внутри metabox’а медиа загрузчик будет показываться, но при выборе картинки ничего не произойдет. А чтобы что-то произошло добавим событие на ajax в следующем разделе.

Сохранение выбранного изображения

Осталось сохранить выбранное изображение id которого мы отправляем с помощью ajax запроса.

WordPress имеем очень удобную систему событий (action’ов) на которые можно повесить свои функции. При ajax запросе по адресу /wp-admin/admin.php если передан параметр action ядро вызывает события с названиями wp_ajax_action_name для авторизованного пользователя и wp_ajax_nopriv_action_name для всех остальных, где action_name значение переданного в ajax значения action.

Пример: Если мы добавим вызов функции на событие wp_ajax_custom_action и с помощью javascript передадим ajax запрос /wp-admin/admin.php?action=custom_action, POST или GET не имеет значения, то wordpress вызовет указанную функцию. Работает легко и удобно — такой вот он, wordpress.

Вооружившись этим знанием добавим последнюю функцию которая будет вызываться на ajax запросе и сохранять id изображения к посту:

/**
 * Ajax callback for attaching/detaching alternative thumbnail to post
 */
function child_theme_ajax_action() {
    $post_ID = intval( $_POST['post_id'] );
    if ( !current_user_can( 'edit_post', $post_ID ) ) {
        wp_die( -1 );
    }

    $thumbnail_id = intval( $_POST['thumbnail_id'] );

    check_ajax_referer( "set_post_alternative_thumbnail", 'nonce' );

    if ( $thumbnail_id == '-1' ) {
        $success = delete_post_meta( $post_ID, '_alt_thumbnail_id' );
    } else {
        $success = update_post_meta( $post_ID, '_alt_thumbnail_id', $thumbnail_id );
    }

    if ( $success ) {
        $return = child_theme_get_alt_thumbnail_html( $thumbnail_id, $post_ID );
        wp_send_json_success( $return );
    }
    wp_die(0);
}

add_action('wp_ajax_set_alternative_thumbnail', 'child_theme_ajax_action');

А вот в этой функции все ну совсем-совсем просто. Берем id поста и изображения из переданных данных, проверяем может ли текущий пользователь редактироваться текущий пост и проверяем nonce, далее удаляем мета данные если переданный id равняется -1, иначе обновляем мета по ключу _alt_thumbnail_id и указываем переданные id изображения. На этом сохранение и удаление изображения уже работает, дальше, если все прошло хорошо, вызываем функции которую вызывали в metabox’е для вывода html кода, и передаем ее вывод обратно в js код где вставляем вывод внутрь metabox’а.

Вот что получается в итоге: Metabox with media uploader example

Для удобного вывода изображения скопируем парочку функций из коры:

/**
 * Display Post Alternative Thumbnail.
 */
function the_post_alt_thumbnail( $size = 'post-thumbnail', $attr = '' ) {
    echo get_the_post_alt_thumbnail( null, $size, $attr );
}


/**
 * Retrieve Post Alternative Thumbnail ID.
 */
function get_post_alt_thumbnail_id( $post_id = null ) {
    $post_id = ( null === $post_id ) ? get_the_ID() : $post_id;

    return get_post_meta( $post_id, '_alt_thumbnail_id', true );
}

/**
 * Retrieve Post Alternative Thumbnail.
 */
function get_the_post_alt_thumbnail( $post_id = null, $size = 'post-thumbnail', $attr = '' ) {
    $post_id           = ( null === $post_id ) ? get_the_ID() : $post_id;
    $post_thumbnail_id = get_post_alt_thumbnail_id( $post_id );

    $size = apply_filters( 'post_thumbnail_size', $size );

    if ( $post_thumbnail_id ) {
        $html = wp_get_attachment_image( $post_thumbnail_id, $size, false, $attr );
    } else {
        $html = '';
    }

    return apply_filters( 'post_thumbnail_html', $html, $post_id, $post_thumbnail_id, $size, $attr );
}

Теперь в любом удобном месте в шаблоне, внутри цикла loop, выводим

<?php the_post_alt_thumbnail(); ?>

В качестве бонуса ниже расскажу как сделать такое же поле но с возможность выбора нескольких изображений.
Картинка для привлечения внимания Multiple featured images in custom metabox

На этом все. Если кто-то что-то не понял то не стесняемся задавать вопросы в комментариях.

Добавляем Media Uploader для custom fields или еще один featured image: 40 комментариев

    1. zviryatko Автор записи

      Про wp.media можно почитать в js файлах в названии которых есть слово media, как таковой документации никто не писал, потому что очень быстро все меняется.

    1. zviryatko Автор записи

      Что именно не работает? Смотри вывод консоли, логи пхп. На днях выложу архив с рабочей версией, если не забуду )

      1. inmtoo

        Приветствую, нашел Ваш пост. Сейчас осваиваю WP и делаю как раз то, что описано в статье. Возникла проблема — не вызывается медиа-загрузчик. Хотя скрипт /js/alternative_image.admin.js подкгрузился. Я все делаю не из функции темы, а из плагина.
        PHP функция подключения js-скрипта:

        function inmtoo_realt_admin_enqueue_scripts_action( $hook ) {
        global $post;

        // check if it’s a post edit page and not any other admin page
        if (
        ( $hook == ‘post-new.php’ || $hook == ‘post.php’ )
        && $post->post_type == ‘realty_residental’
        ) {

        // load media library scripts
        wp_enqueue_media();
        wp_enqueue_style( ‘editor-buttons’ );

        // load our js file
        wp_enqueue_script(
        ‘alternative-image’,
        plugin_dir_url( __FILE__ ) . ‘/js/alternative_image.admin.js’,
        array( ‘jquery’ )
        );

        // add variables to our js file
        wp_localize_script(
        ‘alternative-image’,
        ‘Inmtoo_Realty’,
        array(
        ‘l10n’ => array(
        ‘uploaderTitle’ => __( ‘Set alternative image’, ‘Inmtoo_Realty’ ),
        ‘uploaderButton’ => __( ‘Select image’, ‘Inmtoo_Realty’ ),
        ),
        ‘nonce’ => wp_create_nonce( ‘set_post_alternative_thumbnail_’ . $post->ID ),
        )
        );
        }
        }

        add_action(‘admin_enqueue_scripts’, ‘inmtoo_realt_admin_enqueue_scripts_action’);

        А это сам JS
        (function ($) {
        $(document ).ready(function() {
        var AltImageUpdate = function ( post_id, thumb_id ) {
        wp.media.post( ‘set_alternative_thumbnail’, {
        post_id: post_id,
        thumbnail_id: thumb_id,
        nonce: child_theme.nonce
        } ).done( function ( html ) {
        $(‘#alternative_image’ ).find(‘.inside’ ).html(html);
        } );
        };

        $(‘#alternative_image’ )
        .on(‘click’, ‘#set-post-alternative-thumbnail’, function(e) {
        e.preventDefault();
        e.stopPropagation();

        var uploader = wp.media( {
        title: child_theme.l10n.uploaderTitle,
        button: { text: child_theme.l10n.uploaderButton },
        multiple: true
        } );
        uploader.on(‘select’, function() {
        var attachments = [];
        uploader.state().get( ‘selection’ ).forEach( function ( i ) {
        attachments.push( i.id );
        } );
        AltImageUpdate( wp.media.view.settings.post.id, attachments );
        });
        uploader.open();
        })
        .on(‘click’, ‘#remove-post-alternative-thumbnail’, function(e) {
        e.preventDefault();
        e.stopPropagation();

        AltImageUpdate( wp.media.view.settings.post.id, -1 );
        });
        });
        })(jQuery);

        1. zviryatko Автор записи

          Привет, есть подозрения что в изначальном виде скрипт уже может не работать с новой версие коры, я с вп с того времени особо не работал. Для начала посмотри в js console, может там ошибки есть какие.

  1. Slava

    Скачал, работает) Есть вопрос, как сделать так чтобы картинка прикреплялась как attachment? чтобы я мог вытягивать их через get_posts

    1. zviryatko Автор записи

      Аттачментом ее делать не логично, т.к. аттачмент может только к одному посту принадлежать, но можно в параметрах get_post указывать meta_key, все равно сохраняем как мета информацию

        1. zviryatko Автор записи

          Можно, но нужно будет много чего в js файле переопределить, а еще зачем убирать оттуда вкладку библиотеки? Она же наоборот для удобства сделана, чтобы можно было выбрать существующие изображения, а не новые добавлять каждый раз.

          1. Slava

            Подскажи, можно ли сделать так чтобы при загрузке нового файла через Upload Files он не аттачился к посту нового типа (например слайдер)?

          2. zviryatko Автор записи

            Можно, но зачем? Просто для загрузки файлов есть раздел Медиафайлы. Ну а если очень хочется то на событие сохранение поста типа аттачмент, проверяешь пост парент, если есть и это ид поста нужного типа материала (например slider) то ставишь значение ноль.

    1. zviryatko Автор записи

      Тебе нужно к аттачментам прикреплять еще картинки? О_о
      В вызове add_meta_box четвертым параметром укажи attachment и при добавлении скриптов в функкции child_theme_admin_enqueue_scripts_action поправь проверку post_type тоже на attachment. Будет работать, проверил.

      1. Slava

        Да, нужно аттачить к аттачам)) Поменял, мета бокс появляется при редактировании аттача, загрузчик вызывается, картинка появляется. Но не работает прикрепление, после апдейта не появляется, и не работает удаление(((

        1. zviryatko Автор записи

          Да, забыл о js и post_id…
          В функции child_theme_admin_enqueue_scripts_action после параметра nonce в вызове wp_localize_script добавь еще параметр 'post_id' => $post->ID, а в js файле замени wp.media.view.settings.post.id на child_theme.post_id и должно работать.

          1. Slava

            Спасибо) Заработало, пришлось еще повозиться, у тебя в архиве что ты прикрепил test_theme вместо child_theme ))

          2. zviryatko Автор записи

            Мог легко ошибиться, со времени публикации код очень много раз менялся и за названием темы не слежу… Как-нибудь поправлю =)

      2. Slava

        У меня код с архива, который ты прикреплял)) В произвольных типах записей работает)

  2. Вячеслав

    Привет) подскажи что нужно поменять чтобы я мог урлы передать
    менял на attachments.push( i.url); не выходит(
    uploader.state().get( ‘selection’ ).forEach( function ( i ) {
    attachments.push( i.id );
    } );

    1. zviryatko Автор записи

      А зачем тебе урлы передавать? На стороне пхп ты получаешься id файла, по которому можешь получить его урл.
      А вообще напиши console.log( uploader.state().get( 'selection' ) ) и посмотри что там

  3. Вячеслав

    Мне нужно файлы которые я выбрал прикрепить к посту) При помощи какой функции можно получить урл по id?

  4. Вячеслав

    если написать так то есть title и урл, возвращает данные только первого объекта)
    console.log(uploader.state().get(‘selection’).first().toJSON());

    1. zviryatko Автор записи

      а если бы написал console.log(uploader.state().get(‘selection’).last().toJSON()); то получил бы данные только последнего объекта.

    1. zviryatko Автор записи

      так и стоило сразу делать, а всех сразу тоже можно, uploader.state().get(‘selection’) — возвращает массив с аттачментами, перебираешь и вытягиваешь нужные данные.

  5. Laeis

    Здравствуйте, пример работает вплоть до нажатия кнопки «Select image» — похоже изменился метод формирования Ajax запроса но моих скудных знаний не хватает чтоб это исправить, подскажите как в сегодняшних реалиях должен выглядеть код.

    1. Laeis

      Хотя нет, проверил ваш готовый пример всё работает, значит где-то у меня ошибка

  6. Faradey

    Пример для одной картинки не работает 🙁 Но если в функции child_theme_admin_enqueue_scripts_action

    'nonce' => wp_create_nonce( 'set_post_alternative_thumbnail_' . $post->ID ) заменить на :
    'nonce' => wp_create_nonce( 'set_post_alternative_thumbnail' ) то все заработает

    1. zviryatko Автор записи

      да, ошибочка вышла, спасибо за внимательность, на самом деле лучше заменить во всех проверках check_ajax_referer() параметр и добавить туда id поста.

  7. yura.brd

    Спасибо. Долго искал такого решения! Нужно было для карусели на для каждого поста. Теперь класс!

  8. Slam

    А как на счет выделения файла при редактировании?

    Ладно просто выделить, нужно лишь дописать:
    custom_uploader.on(‘open’,function() {

    var selection = uploader.state().get(‘selection’).first();
    var selected = att_id; // the id of the image
    if (selected) {
    selection.add(wp.media.attachment(selected));
    }
    });
    uploader.open();

    Но если картинка где-то далеко в галерее, так как пост старый, то нужно долго к ней скролить, прежде чем она выделится.
    А вот к примеру, когда в любом старом посте выбираешь изменение Post Thumbnail, то файл выделяет сразу вначале галереи и никуда не нужно листать.
    Вот как такое можно реализовать, знает кто?

      1. Slam

        Вот к примеру, есть морда добавления и редактирования постов с большим количеством картинок.

        Нужно подредактировать какой-то старый пост и изменить картинку (поменять альты, к примеру). Когда открываешь библиотеку, то нужный файл не выделяет до того момента, пока не прокрутить библиотеку к нему.

      2. Slam

        В общем, пошел по более легкому пути.

        Содрал часть кода с FeaturedImage

        wp.media({
        state: ‘featured-image’,
        states: [ new wp.media.controller.FeaturedImage({
        title : ‘Выберите картинку из библиотеки или загрузите новую’,
        editing : false
        })]
        });

        В таком случае wp.media.view.settings.post.featuredImageId нужно назначить ID картинки, которая будет открываться и выделяться ajax запросом при открытии библиотеки.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *