ICT:Drupal opener explained
Custom Media Library Opener for Heritage Digital Assets
In Drupal 11.3.5, a custom "Opener" service allows the Media Library to be used as a standalone modal. This implementation restricts selection to one and only one asset and returns a rendered thumbnail to the parent page, mimicking the "classical" Media Library behavior without the full Field API overhead.
1. Define the Service
Register the opener in your module's your_module.services.yml. You must tag it with media_library.opener so the system can identify it.
services:
your_module.heritage_opener:
class: Drupal\your_module\HeritageAssetOpener
arguments: ['@entity_type.manager']
tags:
- { name: media_library.opener }
2. The Opener Logic
Create src/HeritageAssetOpener.php. This class handles the permission check and generates the AJAX response to inject the thumbnail.
namespace Drupal\your_module;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\media\Entity\Media;
use Drupal\media_library\MediaLibraryOpenerInterface;
use Drupal\media_library\MediaLibraryState;
class HeritageAssetOpener implements MediaLibraryOpenerInterface {
protected $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* Checks if the user can access the media library in this context.
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account) {
return AccessResult::allowedIfHasPermission($account, 'view media');
}
/**
* Logic executed after clicking "Insert" in the modal.
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids) {
$selected_id = reset($selected_ids); // Force single selection logic
$media = Media::load($selected_id);
$response = new AjaxResponse();
if ($media) {
// Build the thumbnail using the 'media_library' view mode
$view_builder = $this->entityTypeManager->getViewBuilder('media');
$render_array = $view_builder->view($media, 'media_library');
// Inject the thumbnail into the UI
$response->addCommand(new HtmlCommand('#asset-preview-container', $render_array));
// Update a hidden field with the ID for form submission
$response->addCommand(new InvokeCommand('#selected-asset-id', 'val', [$selected_id]));
}
return $response;
}
}
3. Triggering the Modal
To launch the modal from a controller or form, build a link using the MediaLibraryState. Setting media_library_remaining to 1 enforces the single-item limit.
use Drupal\Core\Url;
use Drupal\media_library\MediaLibraryState;
// Create the state for our custom opener
$state = MediaLibraryState::create(
'your_module.heritage_opener', // Our service ID
['image', 'document'], // Allowed types
'image', // Default tab
1 // Quantity limit (1 for single asset)
);
$build['select_button'] = [
'#type' => 'link',
'#title' => $this->t('Select Heritage Asset'),
'#url' => Url::fromRoute('media_library.ui', [], ['query' => $state->all()]),
'#attributes' => [
'class' => ['use-ajax', 'button'],
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode(['width' => '80%']),
],
'#attached' => ['library' => ['core/drupal.dialog.ajax']],
];
4. Required HTML Placeholders
Ensure your template or form contains these matching IDs:
id="asset-preview-container": Where the thumbnail will appear.id="selected-asset-id": A hidden input to store the ID for the backend.
Configuration: Custom View Mode
To ensure the selected heritage asset matches the project's design, we use a dedicated view mode instead of the generic library thumbnail.
1. UI Configuration Steps
- Go to Structure > Display modes > View modes and add a new "Media" mode named Template:code.
- Navigate to Structure > Media types > [Your Type] > Manage display.
- Enable the Template:code under Custom display settings.
- Configure the layout:
- Hide all fields except the main file/image.
- Set the Format to Thumbnail and select a custom Image Style (e.g., Template:code).
2. Code Implementation
Update the Template:code method in Template:code to call this specific mode:
// Render the media using our project-specific view mode
$render_array = $view_builder->view($media, 'heritage_asset_preview');
3. CSS Styling
You can now target this specific preview in your theme:
.heritage-thumbnail-wrapper [data-drupal-view-mode="heritage_asset_preview"] {
border: 2px solid #a39161; /* Heritage gold border */
box-shadow: 3px 3px 10px rgba(0,0,0,0.2);
max-width: 300px;
}
Data Integrity: Immutable Selection
To maintain strict archival standards, this tool does not provide a "Remove" button.
- Once an asset is linked, it can only be replaced by launching the Media Library again.
- This prevents "orphaned" Digital Asset nodes in the database that lack an associated file.
- The backend storage should be configured as a Required field to match this UI behavior.
UI Simplification: Hiding the Asset Grid
To prevent historians from being overwhelmed by the full library list during an upload, we apply a "Clean UI" override.
Strategy 1: State Restriction
By restricting Template:code to a single value in the Template:code, the left-hand navigation tabs are removed, focusing the user on the current task.
Strategy 2: CSS Override
We hide the existing asset grid using CSS to ensure the Upload Form is the only interactive element visible.
/* Target the modal specifically when our opener is active */
[data-opener-id="your_module.heritage_opener"] .media-library-view {
display: none; /* Hides the bottom grid of existing files */
}
Strategy 3: "Upload Only" Workflow
If the requirement is to never browse, ensure the user role only has "Create" permissions and not "Access Media Overview", though this may conflict with other Drupal administrative tasks.
UI Simplification: Programmatic List Removal
To avoid CSS conflicts with themes, we use a backend hook to empty the asset grid. This ensures historians only see the Upload Form and not the "long list" of existing files.
1. View Query Alteration
Add the following to Template:code. This intercepts the database query only when our specific opener is active.
function your_module_views_query_alter($view, $query) {
if ($view->id() === 'media_library') {
$state = \Drupal::request()->query->get('media_library_state');
// If our heritage opener is calling the modal, kill the list results
if (isset($state['opener_id']) && $state['opener_id'] === 'your_module.heritage_opener') {
$query->addWhereExpression(0, '1 = 0');
}
}
}
2. Advantages
- Theme Independent: Works with Gin, Claro, or custom heritage skins.
- Performance: The database returns zero rows, making the modal load faster.
- Integrity: Historians cannot accidentally select an existing asset if the requirement is always a fresh upload.
UI Simplification: Removing the Filter Form (Gin Compatibility)
Since the project uses the Gin administration theme, we must avoid CSS-based hiding which may conflict with Gin's dark mode selectors. This hook removes the "Filter" area (exposed filters) from the modal UI at the template level.
1. Template Preprocess Hook
Add this to your Template:code file. It ensures the search/sort inputs are removed before the modal is rendered.
/**
* Implements hook_preprocess_views_view().
* Removes the exposed filter form to keep the UI clean in Gin Dark Mode.
*/
function your_module_preprocess_views_view(&$variables) {
$view = $variables['view'];
if ($view->id() === 'media_library') {
$state = \Drupal::request()->query->get('media_library_state');
// Check if our specific heritage opener is active
if (isset($state['opener_id']) && $state['opener_id'] === 'your_module.heritage_opener') {
// Completely remove the search/filter form from the render array
unset($variables['exposed']);
}
}
}
2. Result
The historian will now see:
- The Upload/Add Media tab (with Gin's native dark mode styling).
- No search/filter bars.
- No long list of existing assets.
- The Insert/Save action buttons at the bottom.