Skip to content

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 presave are 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 the onConfirm prop:
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 with Drupal.ckeditor5.openDialog. A custom CKEditor 5 plugin can point its openDialog at 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.