Extending Media Directories¶
This page is for developers. The browser is a Vue app talking to its own endpoints, so some familiar theming and altering techniques don't reach it — but it exposes deliberate extension points of its own, and everything it creates is a perfectly normal Drupal entity. This page lists the supported ways to plug in.
Media entities are ordinary entities¶
Everything the browser creates or edits is a standard media entity, so all of
core's extension machinery applies: hook_ENTITY_TYPE_presave(),
hook_ENTITY_TYPE_insert(), validation constraints, and so on. If you want to
enforce or derive a value at save time, an ordinary entity hook is the
right tool — no browser-specific API needed.
Two things entity hooks cannot do in the browser, though:
- values set in
presaveare never shown to the editor in the upload form, and - when a media type marks a field as required (for example Alternative text required on an image), the upload form won't let the editor save with the field empty — so a hook that would fill it later never gets the chance.
For anything the editor should see and be able to correct before saving, use the metadata suggestion event below.
Uploaded filenames pass through core's FileUploadSanitizeNameEvent, exactly
like core's own upload paths — existing filename-sanitization subscribers
apply to browser uploads automatically.
Pre-fill upload form metadata¶
When files are added to the upload modal, the browser dispatches
MediaUploadMetadataSuggestionEvent. A subscriber can suggest values for any
upload form field — for example, derive an image's alternative text from its
filename. Suggested values appear pre-filled in the upload form, where the
editor can review and edit them before anything is saved. A field the editor
has already edited is never overwritten.
Only the filenames are available at this point — the files themselves have not been uploaded yet.
namespace Drupal\my_module\EventSubscriber;
use Drupal\media_directories_browser\Event\MediaUploadMetadataSuggestionEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class UploadAltTextSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [MediaUploadMetadataSuggestionEvent::class => 'onSuggest'];
}
public function onSuggest(MediaUploadMetadataSuggestionEvent $event): void {
if ($event->getMediaTypeId() !== 'image') {
return;
}
foreach ($event->getFilenames() as $index => $filename) {
// "red-bicycle_01.jpg" → "Red bicycle 01".
$alt = ucfirst(trim(preg_replace(
'/[-_]+/',
' ',
pathinfo($filename, PATHINFO_FILENAME),
)));
$event->setSuggestion($index, 'field_media_image:alt', $alt);
}
}
}
Register the subscriber in your module's services.yml with the
event_subscriber tag. $event->getFields() lists the form fields available
for the media type; image alt/title sub-fields use a colon separator
(field_media_image:alt).
Feature detection is baked into the page
The browser only makes the suggestion request when at least one subscriber exists — the check happens when the page is built. After enabling or disabling a subscriber module, rebuild caches so the flag is recalculated.
Alter the browser settings¶
Everything the frontend needs to know is attached as
drupalSettings.mediaDirectoriesBrowser in
hook_page_attachments(). The supported way to change any of it is
hook_page_attachments_alter():
function my_module_page_attachments_alter(array &$attachments): void {
if (!isset($attachments['#attached']['drupalSettings']['mediaDirectoriesBrowser'])) {
return;
}
$attachments['#attached']['drupalSettings']['mediaDirectoriesBrowser']['pageSize'] = 250;
}
This is exactly how the AI submodule switches on
enableAiAltText and the per-type AI translation flags — see
MediaDirectoriesAiHooks::pageAttachmentsAlter() for a reference
implementation. The full key list lives in
MediaDirectoriesBrowserHooks::pageAttachments().
Control the quick-edit and bulk-action fields¶
The quick-edit form (used by the field widget) renders the media entity's
media_library form display, and the field pool offered for
bulk actions is limited to fields enabled on that
same display. So which fields editors can touch is a pure site-building
decision: configure Manage form display → Media library on each media type.
Because quick edit is a regular entity form, hook_form_alter() works too —
target it by checking the form operation:
function my_module_form_media_form_alter(array &$form, FormStateInterface $form_state): void {
if ($form_state->getFormObject()->getOperation() === 'media_library') {
// Adjust the quick-edit form.
}
}
Theme the browser¶
The Vue app is styled entirely through CSS custom properties. The app root
gets a class named after the active admin theme:
mdb-theme--<theme_machine_name>. The bundled variants (light, dark,
claro, gin, default_admin) are just stylesheets that set the --mdb-*
variables under their class — and a custom admin theme can do the same with
zero PHP and no rebuild:
/* In your admin theme's CSS, for a theme machine-named "my_admin". */
.mdb-theme--my_admin {
--mdb-color-primary: #0f6292;
--mdb-color-bg: #fbfbfb;
/* ... */
}
The bundled themes in
modules/media_directories_browser/js/src/themes/ are the reference: gin.css
chains Gin's own variables, default_admin.css maps the Default Admin theme's
palette. Ship the overrides in your theme's global stylesheet, or attach them
to the browser's libraries with libraries-extend /
libraries-override against media_directories_browser/media_directories_browser_app
(and .../media_directories_browser_widget for the field widget).
The only Twig on the browser side is the widget's selected-item preview —
media-directories-browser-preview-item.html.twig — which themes can override
like any other template.
Embed the browser in your own UI¶
The compiled app registers a small global API:
window.MediaDirectoriesBrowser—{ createApp, App, Sortable }. Mount the full browser anywhere and receive the selection through theonConfirmprop:
const { createApp, App } = window.MediaDirectoriesBrowser;
createApp(App, {
onConfirm(selectedItems) {
// selectedItems: array of media item objects (uuid, attributes, ...).
},
}).mount(container);
The module's own field widget (js/media_directories_browser.widget.js) and
CKEditor integration (js/media_directories_browser.ckeditor.js) are the
two in-tree examples of this pattern, including the props they pass.
-
Drupal.media_directories_browser.openDialog(url, saveCallback, dialogSettings)— opens the browser in a modal, signature-compatible withDrupal.ckeditor5.openDialog. A custom CKEditor 5 plugin can point itsopenDialogat this function to reuse the browser as its media picker. -
Drupal.mediaDirectoriesBrowserWidget.updateState(widget)— for integrations that re-render the field widget's markup outside Drupal's control (Drupal Canvas does this) and need to re-sync the widget state afterwards.
Attach the media_directories_browser/media_directories_browser_app library
to get the globals.
Work with the directory field¶
The base module adds a directory entity reference (taxonomy term) base field
to every media entity. You can read, write and query it like any other field —
$media->set('directory', $tid) files a media item programmatically.
The tree root is represented by the constant
\Drupal\media_directories\MediaDirectoryRoot::VALUE (-1, because 0 reads
as empty in too many places) — use the constant, not the number. For Views,
the module registers a media_directory filter and contextual argument that
understand the root sentinel and depth.
Services¶
The PHP layer is plain injectable services (see the *.services.yml files),
usable from your own code or decoratable when you need to change behavior
wholesale — media_directories_browser.media_service (CRUD, uploads),
.media_type_service (type/field discovery, suggestion dispatch),
.directory_service (tree operations), .translation_service,
media_directories_ai.alt_text, and friends.
What is not a stable API¶
The HTTP endpoints under /api/media-directories-browser/* are the private
contract between the Vue app and the module — they are unversioned and may
change in any release. The same goes for the internals of the compiled
dist/ bundle. Build on the extension points above instead; if you're missing
one, open a feature request.