wp-admin-media-frame
Open the standard WordPress Media Library picker from plugin
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-admin-media-frame
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WordPress Admin Media Picker (wp.media)
The Media Library modal is the same Backbone-driven UI WP uses for "Add Media" on the post editor. Plugins reuse it for logo pickers, avatar fields, gallery builders, per-row icon selectors — anything that wants "open the WP media library, let the user pick or upload, hand me back an attachment". The blocker is almost always the bootstrap, not the API.
When to use this skill
Trigger when ANY of the following is true:
- A plugin admin page needs to pick an image / file / video / audio from the WP Media Library.
- The user is adding a "Choose image", "Upload logo", "Select gallery", "Pick avatar", "Browse media" button to a settings page, metabox, or repeater row.
- Code references
wp.media,wp.media.frame,wp_enqueue_media,wp_prepare_attachment_for_js,frame.state().get( 'selection' ),library: { type: ... },multiple: 'add', or theMediaFrame.Select/MediaFrame.Posttypes. - The user has a textarea / hidden input for an attachment ID and needs the UI around it.
- The user complains: "wp.media is undefined", "the modal opens but the Select button does nothing", "I get the URL but not the right size".
The bootstrap — three pieces
Like every other WP admin JS API, the media frame needs (1) a PHP enqueue, (2) the right asset deps in your JS, (3) the JS init at DOM-ready. Miss any one and you get wp.media is undefined or a silent no-op.
1. PHP — call wp_enqueue_media() on YOUR screen only
wp_enqueue_media() is idempotent (it guards on did_action( 'wp_enqueue_media' )), but it enqueues ~12 scripts and a stylesheet. Don't call it globally.
add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {
if ( 'settings_page_myplugin' !== $hook_suffix ) {
return;
}
wp_enqueue_media();
wp_enqueue_script(
'myplugin-media-picker',
plugins_url( 'assets/media-picker.js', MYPLUGIN_FILE ),
array( 'jquery', 'media-editor', 'wp-i18n' ),
MYPLUGIN_VERSION,
array( 'in_footer' => true )
);
} );
Declare media-editor as a dependency because it supplies the editor-facing
media API and depends on the underlying media-views stack. It is not a
lightweight alternative to that stack; wp_enqueue_media() loads the media
models, views, settings, templates, and styles required by the frame.
2. The HTML scaffold
The picker needs a trigger button, a hidden input to store the attachment ID, and a preview spot. Keep the input as the source of truth — server-side you save the ID, not the URL.
<div class="myplugin-image-field" data-target="logo">
<input
type="hidden"
id="myplugin_logo_id"
name="myplugin_options[logo_id]"
value="<?php echo esc_attr( $options['logo_id'] ?? '' ); ?>"
/>
<div class="myplugin-image-preview">
<?php
if ( ! empty( $options['logo_id'] ) ) {
echo wp_get_attachment_image( (int) $options['logo_id'], 'thumbnail' );
}
?>
</div>
<button type="button" class="button myplugin-image-pick">
<?php esc_html_e( 'Choose image', 'myplugin' ); ?>
</button>
<button type="button" class="button myplugin-image-remove">
<?php esc_html_e( 'Remove', 'myplugin' ); ?>
</button>
</div>
3. JS — open the frame on click
jQuery( function ( $ ) {
let frame;
$( '.myplugin-image-pick' ).on( 'click', function ( e ) {
e.preventDefault();
// Cache the frame — opening a new one every click is wasteful and
// loses the "previously selected" state.
if ( frame ) {
frame.open();
return;
}
frame = wp.media( {
title: wp.i18n.__( 'Choose image', 'myplugin' ),
button: { text: wp.i18n.__( 'Use this image', 'myplugin' ) },
library: { type: 'image' },
multiple: false,
} );
frame.on( 'select', function () {
const attachment = frame.state().get( 'selection' ).first().toJSON();
// Store the ID — the source of truth.
$( '#myplugin_logo_id' ).val( attachment.id );
// Render a thumbnail preview. CRITICAL: pick the right size — see below.
const thumb = attachment.sizes && attachment.sizes.thumbnail
? attachment.sizes.thumbnail.url
: attachment.url;
$( '.myplugin-image-preview' )
.empty()
.append( $( '<img>', { src: thumb, alt: '' } ) );
} );
frame.open();
} );
$( '.myplugin-image-remove' ).on( 'click', function ( e ) {
e.preventDefault();
$( '#myplugin_logo_id' ).val( '' );
$( '.myplugin-image-preview' ).empty();
} );
} );
Picking the right frame type
wp.media( { frame: 'select', ... } ) is the default and covers almost every plugin picker. Use 'post' only when re-creating the classic-editor Add Media flow, and avoid internal frames such as 'manage' / 'edit-attachments' in normal plugin settings screens.
Filtering the library
The library attribute is a wp.media.query filter. Common shapes: library: { type: 'image' }, library: { type: [ 'image', 'video' ] }, library: { type: 'application/pdf' }, library: { uploadedTo: postId }, and library: { author: MyPluginMedia.currentUserId }.
Localize MyPluginMedia.currentUserId from PHP with get_current_user_id() if you need an author filter. Do not read it from wp.media.view.settings.post.featuredImageId — that value is an attachment/post ID, not a user ID.
The client-side media query layer recognizes a curated set of props (search, type, perPage, menuOrder, uploadedTo, status, include, exclude, author) and maps some of them to query vars such as s. Do not assume arbitrary WP_Query attachment args will work from library.
Single vs multi-select
// Single. The default.
multiple: false
// Multi-select with normal toggle behavior (re-clicking deselects).
multiple: true
// Multi-select where re-clicking does NOT deselect — useful for "add to gallery".
multiple: 'add'
For multi-select, iterate the selection collection:
frame.on( 'select', function () {
const attachments = frame.state().get( 'selection' ).toJSON();
attachments.forEach( function ( attachment ) {
// attachment.id, attachment.url, attachment.title, attachment.sizes, ...
} );
} );
Pre-selecting an existing attachment on reopen
When the user already picked an image and reopens the picker, you want that image highlighted in the library — not a blank grid. Hook into open and add the attachment to the selection:
frame.on( 'open', function () {
const selection = frame.state().get( 'selection' );
selection.reset();
const currentId = parseInt( $( '#myplugin_logo_id' ).val(), 10 );
if ( ! currentId ) {
return;
}
const attachment = wp.media.attachment( currentId );
attachment.fetch(); // hydrate through core's get-attachment AJAX action if not in cache
selection.add( attachment );
} );
wp.media.attachment( id ) returns a Backbone model; .fetch() pulls the data through core's get-attachment admin-ajax action (cached after first call).
What you get from selection.first().toJSON()
The same shape wp_prepare_attachment_for_js() returns server-side. Useful fields for plugin code:
| Field | What it is |
|---|---|
id |
Attachment post ID — the value you save |
url |
URL of the ORIGINAL file (full resolution) |
title / alt / caption / description |
User-facing metadata |
mime / type / subtype |
'image/png' / 'image' / 'png' |
filename |
File basename |
filesizeInBytes / filesizeHumanReadable |
Size info |
width / height |
Dimensions of the original (images/videos only) |
sizes |
Map of exposed image sizes → { url, width, height, orientation, ... }. Core exposes thumbnail, medium, large, and full when metadata exists; custom sizes only appear if they are exposed through image_size_names_choose |
link |
Public attachment page URL |
uploadedTo |
Parent post ID (if attached to a post) |
author |
User ID who uploaded |
The pitfall: attachment.url is ALWAYS the full-size URL. To get a thumbnail, dig into attachment.sizes.thumbnail.url. Production preview code should fall back gracefully (some attachments, especially non-images or SVGs without thumbnails, don't have all sizes registered).
function getDisplayUrl( attachment, sizeName = 'thumbnail' ) {
if ( attachment.sizes && attachment.sizes[ sizeName ] ) {
return attachment.sizes[ sizeName ].url;
}
if ( attachment.sizes && attachment.sizes.medium ) {
return attachment.sizes.medium.url;
}
return attachment.url; // fallback to original
}
The Backbone events you can hook
Use select for actual picks, open for preselecting an existing attachment, and close only for cleanup or refocusing. Do not save on close; cancellation fires it too. See reference.md for the event table.
WordPress 7.1 enables Media Library infinite scrolling by default, with a
per-user opt-out and the media_library_infinite_scrolling filter. Do not assume
all attachments or a final page are already loaded into a frame collection.
Read reference.md for preference/filter precedence and test implications.
Saving and rendering server-side
Save the ID, never the URL. Sanitize with absint(), verify that it is an
attachment of the allowed MIME/type, and enforce the authorization appropriate
to the setting (for example, whether the current user may use or edit that
attachment). A post_type = attachment check alone does not establish access
or image-ness. Render with wp_get_attachment_image() and use
wp_get_attachment_image_url( $id, $size ) only when you truly need a raw URL.
See reference.md for the snippets.
Critical rules
- Always call
wp_enqueue_media()before any code that toucheswp.media. The cause of 90% of "wp.media is undefined" reports. - Save the ID, not the URL. The URL rots with site moves, CDNs, and uploads-folder relocations. The ID is immutable.
- Cache the frame instance. Re-creating a new frame on every button click creates ~12 Backbone views per click, loses the previous selection, and visibly stutters.
attachment.urlis the FULL-size URL. Useattachment.sizes.<size>.urlfor any other size, with a fallback for attachments that don't have that size registered.- Listen to
select, notclose.closefires on cancel too — you'll save a phantom value. multiple: 'add'is NOT a typo fortrue. They're three distinct modes —false(single),true(multi with deselect),'add'(multi without deselect, the gallery builder mode).- Don't open a frame before
DOMContentLoaded. Translations and modal containers may not be ready. - Don't reach inside
wp.media.view.*to build a custom frame unless you've read media-views.js. The Backbone architecture is undocumented in places and changes between WP versions. For 95% of plugin needs,wp.media( { frame, library, multiple } )is enough.
Common AI mistakes
See reference.md for before/after snippets covering implicit wp.media() defaults, saving URLs instead of IDs, missing wp_enqueue_media(), recreating frames on every click, and assuming attachment.sizes.thumbnail always exists.
Pattern: a per-row picker in a repeater
Use one cached frame, but track the active row before opening it. On select, write the chosen attachment ID into that row's hidden input. See reference.md for the full delegated-click example.
Cross-references
- See
wp-plugin-assets-loadingfor the$hook_suffixenqueue gate. - See
wp-admin-settings-apiwhen the picker lives inside an options page; the hidden input goes through thesanitize_callback. - See
wp-admin-drag-and-dropwhen building a gallery with reorderable thumbnails —wp.mediagives you the IDs, sortable gives you the order. - See
wp-client-side-media-processingfor WordPress 7.1 browser-side image processing and REST finalization; it is separate from selecting an existing attachment.
What this skill does NOT cover
- Custom Backbone frames extending
wp.media.view.MediaFrame.Select. Doable but undocumented; almost never needed. - The Customizer's media controls (
wp.customize.MediaControl). Different abstraction layer. - Programmatic uploads (
wp_handle_upload,media_handle_upload). That's a PHP-side topic. - The block editor's media handling. Blocks use
<MediaUpload>from@wordpress/media-utils— that wraps the same Backbone frame but exposes a React-ergonomic API. Out of scope for classic admin pages.
References
wp-includes/media.php—wp_enqueue_media(),wp_prepare_attachment_for_js(), and Media Library settings.wp-includes/js/media-models.js—wp.media()entry point and frame-type switch.wp-includes/js/media-views.js— the Backbone views; useful when you actually need to subclass.wp-includes/script-loader.php—media-editor,media-views,media-modelshandle registrations.reference.md— server render snippets, event table, per-row picker, and common mistakes.- Official documentation: https://developer.wordpress.org/reference/functions/wp_enqueue_media/
- Official documentation: https://developer.wordpress.org/reference/functions/wp_prepare_attachment_for_js/
- Official documentation: https://codex.wordpress.org/Javascript_Reference/wp.media
Files (wp-agent-skills)
-
reference.md 5.6 KB
# wp.media Reference Examples ## Frame Types | `frame` | Purpose | When a plugin uses it | |---|---|---| | `'select'` | Basic picker; pick existing attachment or upload | Most plugin scenarios | | `'post'` | Classic editor Add Media frame with editor sidebars | Re-creating classic-editor media flow | | `'image'` / `'audio'` / `'video'` | Edit details of an existing attachment of that type | Rare | | `'edit-attachments'` | Bulk-edit a list of attachments | Internal core use | | `'manage'` | Media Library grid view | Internal core use | ## Event Table | Event | When it fires | Common use | |---|---|---| | `open` | After modal is rendered and visible | Pre-select existing item, set library state | | `select` | User clicked the Select button | Read selection, save attachment ID | | `close` | Modal closed for any reason | Cleanup, refocus trigger button | | `escape` | User pressed Esc | Rarely needed; `close` covers most cases | | `ready` | Frame finished initial render | Customize internal views before user sees them | The toolbar Select button dispatches `select`. Do not listen to `close` for "user picked something"; `close` fires on cancellation too. ## Infinite scrolling in WordPress 7.1 Core initializes Media Library infinite scrolling to true. A user can opt out through the profile personal option, stored so that the literal string `'false'` disables it. Core then applies: ```php apply_filters( 'media_library_infinite_scrolling', $infinite_scrolling ); ``` The filter runs after the user preference, so an extension can technically override either state. Do that only for a documented site-wide reason; silently undoing a user's accessibility/performance preference is hostile. Audit code that listens for a one-time "load more" interaction or assumes the initial Backbone collection is complete. Infinite loading changes timing, not the server's collection limits. Keep search/filter state stable, debounce expensive observers, avoid per-attachment DOM scans on every append, and test: - default infinite mode and the user's opt-out; - empty, one-page, and many-page libraries; - switching MIME/search/author filters while a request is in flight; - keyboard and screen-reader access to newly appended items; - a slow/failed next-page request without duplicate models. ## Saving and Rendering Server-Side ```php $sanitize_logo_id = static function ( $value ): int { $id = absint( $value ); if ( ! current_user_can( 'upload_files' ) || ! $id || 'attachment' !== get_post_type( $id ) || ! wp_attachment_is_image( $id ) ) { return 0; } return $id; }; ``` ```php if ( ! empty( $options['logo_id'] ) ) { echo wp_get_attachment_image( (int) $options['logo_id'], 'medium', false, array( 'class' => 'myplugin-logo' ) ); } $logo_url = wp_get_attachment_image_url( (int) $options['logo_id'], 'full' ); ``` ## Per-Row Picker in a Repeater ```js jQuery( function ( $ ) { let frame, $activeRow; $( '#myplugin-rows' ).on( 'click', '.row-pick-image', function ( e ) { e.preventDefault(); $activeRow = $( this ).closest( '.myplugin-row' ); if ( ! frame ) { frame = wp.media( { title: wp.i18n.__( 'Choose row image', 'myplugin' ), library: { type: 'image' }, multiple: false, } ); frame.on( 'select', function () { if ( ! $activeRow ) { return; } const attachment = frame.state().get( 'selection' ).first().toJSON(); const thumb = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; $activeRow.find( '.row-image-id' ).val( attachment.id ); $activeRow.find( '.row-image-preview' ) .empty() .append( $( '<img>', { src: thumb, alt: '' } ) ); } ); frame.on( 'close', function () { $activeRow = null; } ); } frame.open(); } ); } ); ``` ## Common Mistakes ```js // WRONG: relies on implicit defaults and opens an unconstrained library. const frame = wp.media(); // RIGHT: be explicit. const frame = wp.media( { frame: 'select', library: { type: 'image' }, multiple: false } ); ``` ```js // WRONG: saves the URL; breaks on site move. $( '#logo_url' ).val( attachment.url ); // RIGHT: save the ID. $( '#logo_id' ).val( attachment.id ); ``` ```js // WRONG: wp.media is undefined because wp_enqueue_media() was not called. $( '.pick-image' ).on( 'click', () => wp.media().open() ); // RIGHT: guard, and enqueue media on this screen. if ( ! window.wp || ! wp.media ) { return; } ``` ```js // WRONG: fresh frame every click; selection state is lost. $( '.pick-image' ).on( 'click', function () { wp.media( { library: { type: 'image' } } ).on( 'select', handleSelect ).open(); } ); // RIGHT: close over a frame variable. let frame; $( '.pick-image' ).on( 'click', function () { if ( ! frame ) { frame = wp.media( { library: { type: 'image' } } ); frame.on( 'select', handleSelect ); } frame.open(); } ); ``` ```js // WRONG: assumes thumbnail always exists. $preview.html( `<img src="${ attachment.sizes.thumbnail.url }">` ); // RIGHT: fall back through sizes. const src = ( attachment.sizes && attachment.sizes.thumbnail && attachment.sizes.thumbnail.url ) || ( attachment.sizes && attachment.sizes.medium && attachment.sizes.medium.url ) || attachment.url; $preview.empty().append( $( '<img>', { src, alt: '' } ) ); ``` -
SKILL.md 14.4 KB
--- name: wp-admin-media-frame description: Open the standard WordPress Media Library picker from plugin admin UI with `wp_enqueue_media()` and `wp.media()`. Covers the screen-gated enqueue, `media-editor` dependency, `wp.media( { frame, title, button, library, multiple } )`, `library` filters for type / MIME / uploadedTo / author, `multiple` values `true` and `'add'`, `select` and `open` events, `frame.state().get( 'selection' ).first().toJSON()`, attachment `sizes`, frame caching, pre-selecting existing attachments, and saving attachment IDs instead of URLs. Use for image, file, gallery, logo, avatar, cover, or per-row icon pickers in settings pages, metaboxes, and repeaters. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "wordpress" wp-skills-plugin-version-tested: "6.0 - 7.1" wp-skills-wp-version-tested: "7.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-20" --- # WordPress Admin Media Picker (`wp.media`) The Media Library modal is the same Backbone-driven UI WP uses for "Add Media" on the post editor. Plugins reuse it for logo pickers, avatar fields, gallery builders, per-row icon selectors — anything that wants "open the WP media library, let the user pick or upload, hand me back an attachment". The blocker is almost always the bootstrap, not the API. ## When to use this skill Trigger when ANY of the following is true: - A plugin admin page needs to pick an image / file / video / audio from the WP Media Library. - The user is adding a "Choose image", "Upload logo", "Select gallery", "Pick avatar", "Browse media" button to a settings page, metabox, or repeater row. - Code references `wp.media`, `wp.media.frame`, `wp_enqueue_media`, `wp_prepare_attachment_for_js`, `frame.state().get( 'selection' )`, `library: { type: ... }`, `multiple: 'add'`, or the `MediaFrame.Select` / `MediaFrame.Post` types. - The user has a textarea / hidden input for an attachment ID and needs the UI around it. - The user complains: "wp.media is undefined", "the modal opens but the Select button does nothing", "I get the URL but not the right size". ## The bootstrap — three pieces Like every other WP admin JS API, the media frame needs (1) a PHP enqueue, (2) the right asset deps in your JS, (3) the JS init at DOM-ready. Miss any one and you get `wp.media is undefined` or a silent no-op. ### 1. PHP — call `wp_enqueue_media()` on YOUR screen only `wp_enqueue_media()` is idempotent (it guards on `did_action( 'wp_enqueue_media' )`), but it enqueues ~12 scripts and a stylesheet. Don't call it globally. ```php add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void { if ( 'settings_page_myplugin' !== $hook_suffix ) { return; } wp_enqueue_media(); wp_enqueue_script( 'myplugin-media-picker', plugins_url( 'assets/media-picker.js', MYPLUGIN_FILE ), array( 'jquery', 'media-editor', 'wp-i18n' ), MYPLUGIN_VERSION, array( 'in_footer' => true ) ); } ); ``` Declare `media-editor` as a dependency because it supplies the editor-facing media API and depends on the underlying `media-views` stack. It is not a lightweight alternative to that stack; `wp_enqueue_media()` loads the media models, views, settings, templates, and styles required by the frame. ### 2. The HTML scaffold The picker needs a trigger button, a hidden input to store the attachment ID, and a preview spot. Keep the input as the source of truth — server-side you save the ID, not the URL. ```php <div class="myplugin-image-field" data-target="logo"> <input type="hidden" id="myplugin_logo_id" name="myplugin_options[logo_id]" value="<?php echo esc_attr( $options['logo_id'] ?? '' ); ?>" /> <div class="myplugin-image-preview"> <?php if ( ! empty( $options['logo_id'] ) ) { echo wp_get_attachment_image( (int) $options['logo_id'], 'thumbnail' ); } ?> </div> <button type="button" class="button myplugin-image-pick"> <?php esc_html_e( 'Choose image', 'myplugin' ); ?> </button> <button type="button" class="button myplugin-image-remove"> <?php esc_html_e( 'Remove', 'myplugin' ); ?> </button> </div> ``` ### 3. JS — open the frame on click ```js jQuery( function ( $ ) { let frame; $( '.myplugin-image-pick' ).on( 'click', function ( e ) { e.preventDefault(); // Cache the frame — opening a new one every click is wasteful and // loses the "previously selected" state. if ( frame ) { frame.open(); return; } frame = wp.media( { title: wp.i18n.__( 'Choose image', 'myplugin' ), button: { text: wp.i18n.__( 'Use this image', 'myplugin' ) }, library: { type: 'image' }, multiple: false, } ); frame.on( 'select', function () { const attachment = frame.state().get( 'selection' ).first().toJSON(); // Store the ID — the source of truth. $( '#myplugin_logo_id' ).val( attachment.id ); // Render a thumbnail preview. CRITICAL: pick the right size — see below. const thumb = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; $( '.myplugin-image-preview' ) .empty() .append( $( '<img>', { src: thumb, alt: '' } ) ); } ); frame.open(); } ); $( '.myplugin-image-remove' ).on( 'click', function ( e ) { e.preventDefault(); $( '#myplugin_logo_id' ).val( '' ); $( '.myplugin-image-preview' ).empty(); } ); } ); ``` ## Picking the right frame type `wp.media( { frame: 'select', ... } )` is the default and covers almost every plugin picker. Use `'post'` only when re-creating the classic-editor Add Media flow, and avoid internal frames such as `'manage'` / `'edit-attachments'` in normal plugin settings screens. ## Filtering the library The `library` attribute is a `wp.media.query` filter. Common shapes: `library: { type: 'image' }`, `library: { type: [ 'image', 'video' ] }`, `library: { type: 'application/pdf' }`, `library: { uploadedTo: postId }`, and `library: { author: MyPluginMedia.currentUserId }`. Localize `MyPluginMedia.currentUserId` from PHP with `get_current_user_id()` if you need an author filter. Do not read it from `wp.media.view.settings.post.featuredImageId` — that value is an attachment/post ID, not a user ID. The client-side media query layer recognizes a curated set of props (`search`, `type`, `perPage`, `menuOrder`, `uploadedTo`, `status`, `include`, `exclude`, `author`) and maps some of them to query vars such as `s`. Do not assume arbitrary `WP_Query` attachment args will work from `library`. ## Single vs multi-select ```js // Single. The default. multiple: false // Multi-select with normal toggle behavior (re-clicking deselects). multiple: true // Multi-select where re-clicking does NOT deselect — useful for "add to gallery". multiple: 'add' ``` For multi-select, iterate the selection collection: ```js frame.on( 'select', function () { const attachments = frame.state().get( 'selection' ).toJSON(); attachments.forEach( function ( attachment ) { // attachment.id, attachment.url, attachment.title, attachment.sizes, ... } ); } ); ``` ## Pre-selecting an existing attachment on reopen When the user already picked an image and reopens the picker, you want that image highlighted in the library — not a blank grid. Hook into `open` and add the attachment to the selection: ```js frame.on( 'open', function () { const selection = frame.state().get( 'selection' ); selection.reset(); const currentId = parseInt( $( '#myplugin_logo_id' ).val(), 10 ); if ( ! currentId ) { return; } const attachment = wp.media.attachment( currentId ); attachment.fetch(); // hydrate through core's get-attachment AJAX action if not in cache selection.add( attachment ); } ); ``` `wp.media.attachment( id )` returns a Backbone model; `.fetch()` pulls the data through core's `get-attachment` admin-ajax action (cached after first call). ## What you get from `selection.first().toJSON()` The same shape `wp_prepare_attachment_for_js()` returns server-side. Useful fields for plugin code: | Field | What it is | |---|---| | `id` | Attachment post ID — the value you save | | `url` | URL of the ORIGINAL file (full resolution) | | `title` / `alt` / `caption` / `description` | User-facing metadata | | `mime` / `type` / `subtype` | `'image/png'` / `'image'` / `'png'` | | `filename` | File basename | | `filesizeInBytes` / `filesizeHumanReadable` | Size info | | `width` / `height` | Dimensions of the original (images/videos only) | | `sizes` | Map of exposed image sizes → `{ url, width, height, orientation, ... }`. Core exposes `thumbnail`, `medium`, `large`, and `full` when metadata exists; custom sizes only appear if they are exposed through `image_size_names_choose` | | `link` | Public attachment page URL | | `uploadedTo` | Parent post ID (if attached to a post) | | `author` | User ID who uploaded | The pitfall: `attachment.url` is ALWAYS the full-size URL. To get a thumbnail, dig into `attachment.sizes.thumbnail.url`. Production preview code should fall back gracefully (some attachments, especially non-images or SVGs without thumbnails, don't have all sizes registered). ```js function getDisplayUrl( attachment, sizeName = 'thumbnail' ) { if ( attachment.sizes && attachment.sizes[ sizeName ] ) { return attachment.sizes[ sizeName ].url; } if ( attachment.sizes && attachment.sizes.medium ) { return attachment.sizes.medium.url; } return attachment.url; // fallback to original } ``` ## The Backbone events you can hook Use `select` for actual picks, `open` for preselecting an existing attachment, and `close` only for cleanup or refocusing. Do not save on `close`; cancellation fires it too. See `reference.md` for the event table. WordPress 7.1 enables Media Library infinite scrolling by default, with a per-user opt-out and the `media_library_infinite_scrolling` filter. Do not assume all attachments or a final page are already loaded into a frame collection. Read `reference.md` for preference/filter precedence and test implications. ## Saving and rendering server-side Save the **ID**, never the URL. Sanitize with `absint()`, verify that it is an attachment of the allowed MIME/type, and enforce the authorization appropriate to the setting (for example, whether the current user may use or edit that attachment). A `post_type = attachment` check alone does not establish access or image-ness. Render with `wp_get_attachment_image()` and use `wp_get_attachment_image_url( $id, $size )` only when you truly need a raw URL. See `reference.md` for the snippets. ## Critical rules - **Always call `wp_enqueue_media()` before any code that touches `wp.media`**. The cause of 90% of "wp.media is undefined" reports. - **Save the ID, not the URL**. The URL rots with site moves, CDNs, and uploads-folder relocations. The ID is immutable. - **Cache the frame instance**. Re-creating a new frame on every button click creates ~12 Backbone views per click, loses the previous selection, and visibly stutters. - **`attachment.url` is the FULL-size URL**. Use `attachment.sizes.<size>.url` for any other size, with a fallback for attachments that don't have that size registered. - **Listen to `select`, not `close`**. `close` fires on cancel too — you'll save a phantom value. - **`multiple: 'add'` is NOT a typo for `true`**. They're three distinct modes — `false` (single), `true` (multi with deselect), `'add'` (multi without deselect, the gallery builder mode). - **Don't open a frame before `DOMContentLoaded`**. Translations and modal containers may not be ready. - **Don't reach inside `wp.media.view.*` to build a custom frame** unless you've read media-views.js. The Backbone architecture is undocumented in places and changes between WP versions. For 95% of plugin needs, `wp.media( { frame, library, multiple } )` is enough. ## Common AI mistakes See `reference.md` for before/after snippets covering implicit `wp.media()` defaults, saving URLs instead of IDs, missing `wp_enqueue_media()`, recreating frames on every click, and assuming `attachment.sizes.thumbnail` always exists. ## Pattern: a per-row picker in a repeater Use one cached frame, but track the active row before opening it. On `select`, write the chosen attachment ID into that row's hidden input. See `reference.md` for the full delegated-click example. ## Cross-references - See **`wp-plugin-assets-loading`** for the `$hook_suffix` enqueue gate. - See **`wp-admin-settings-api`** when the picker lives inside an options page; the hidden input goes through the `sanitize_callback`. - See **`wp-admin-drag-and-drop`** when building a gallery with reorderable thumbnails — `wp.media` gives you the IDs, sortable gives you the order. - See **`wp-client-side-media-processing`** for WordPress 7.1 browser-side image processing and REST finalization; it is separate from selecting an existing attachment. ## What this skill does NOT cover - Custom Backbone frames extending `wp.media.view.MediaFrame.Select`. Doable but undocumented; almost never needed. - The Customizer's media controls (`wp.customize.MediaControl`). Different abstraction layer. - Programmatic uploads (`wp_handle_upload`, `media_handle_upload`). That's a PHP-side topic. - The block editor's media handling. Blocks use `<MediaUpload>` from `@wordpress/media-utils` — that wraps the same Backbone frame but exposes a React-ergonomic API. Out of scope for classic admin pages. ## References - `wp-includes/media.php` — `wp_enqueue_media()`, `wp_prepare_attachment_for_js()`, and Media Library settings. - `wp-includes/js/media-models.js` — `wp.media()` entry point and frame-type switch. - `wp-includes/js/media-views.js` — the Backbone views; useful when you actually need to subclass. - `wp-includes/script-loader.php` — `media-editor`, `media-views`, `media-models` handle registrations. - `reference.md` — server render snippets, event table, per-row picker, and common mistakes. - Official documentation: <https://developer.wordpress.org/reference/functions/wp_enqueue_media/> - Official documentation: <https://developer.wordpress.org/reference/functions/wp_prepare_attachment_for_js/> - Official documentation: <https://codex.wordpress.org/Javascript_Reference/wp.media>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.