Jump to content

ICT:Drupal opener explained: Difference between revisions

From Costa Sano MediaWiki
No edit summary
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 135: Line 135:
</source>
</source>


=== 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 {{code|allowed_media_types}} to a single value in the {{code|MediaLibraryState}}, 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.
<source lang="css">
/* 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 */
}
</source>
=== 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 {{code|your_module.module}}. This intercepts the database query only when our specific opener is active.
<source lang="php">
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');
    }
  }
}
</source>
=== 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 {{code|your_module.module}} file. It ensures the search/sort inputs are removed before the modal is rendered.
<source lang="php">
/**
* 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']);
    }
  }
}
</source>
=== 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.
== Asset Metadata: Mandatory Captions ==
To maintain archival integrity, the '''Description/Caption''' field is mandatory. This metadata is captured during upload and serves as the primary title for the asset context.
=== 1. UI Enforcement ===
* Navigate to '''Structure > Media types > [Your Type] > Manage fields'''.
* Ensure the field used for captions is set to {{code|Required}}.
* The "Insert" button in the modal will remain disabled or trigger a validation error if this field is empty.
=== 2. Form Optimization (Gin Theme) ===
To provide the cleanest experience in '''Gin Dark Mode''', only the essential fields should be visible during upload:
# Go to '''Manage form display''' for the Media Type.
# Select the {{code|Media library}} custom display mode.
# Disable all secondary fields, leaving only:
#* '''File/Image'''
#* '''Alternative Text / Description''' (The Caption)
=== 3. Backend Relationship ===
The Opener passes this metadata along with the Media ID. When the asset is saved, the Drupal Media Entity holds this "Caption" as its primary metadata, ensuring the file and its description are never decoupled.
== Data Synchronization: Media Metadata Mapping ==
To ensure the historian's caption is used as the official Title of the Digital Asset, we use a presave hook to synchronize the data.
=== 1. Implementation Logic ===
Add this hook to {{code|your_module.module}}. It automatically pulls the "Alt" text from the selected media and updates the Node's title field.
<source lang="php">
function your_module_node_presave($node) {
  if ($node->bundle() === 'digital_asset') {
    $media_id = $node->get('field_heritage_media')->target_id;
    if ($media_id) {
      $media = \Drupal\media\Entity\Media::load($media_id);
      // Retrieve the Alt text from the image source field
      $caption = $media->getSource()->getSourceFieldValue($media);
      // Sync to the node title
      $node->setTitle($caption);
    }
  }
}
</source>
=== 2. Requirements ===
* The {{code|field_heritage_media}} must be populated during the selection process.
* The Media entity must have a valid value in its source field (e.g., Alt text).
[[Category:Backend Logic]]
[[Category:Data Mapping]]
[[Category:Gin Theme Compatibility]]
[[Category:Drupal Development]]
[[Category:Drupal Development]]
[[Category:Heritage Project Documentation]]
[[Category:Heritage Project Documentation]]

Latest revision as of 10:09, 12 March 2026

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

  1. Go to Structure > Display modes > View modes and add a new "Media" mode named Template:code.
  2. Navigate to Structure > Media types > [Your Type] > Manage display.
  3. Enable the Template:code under Custom display settings.
  4. 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:

  1. The Upload/Add Media tab (with Gin's native dark mode styling).
  2. No search/filter bars.
  3. No long list of existing assets.
  4. The Insert/Save action buttons at the bottom.

Asset Metadata: Mandatory Captions

To maintain archival integrity, the Description/Caption field is mandatory. This metadata is captured during upload and serves as the primary title for the asset context.

1. UI Enforcement

  • Navigate to Structure > Media types > [Your Type] > Manage fields.
  • Ensure the field used for captions is set to Template:code.
  • The "Insert" button in the modal will remain disabled or trigger a validation error if this field is empty.

2. Form Optimization (Gin Theme)

To provide the cleanest experience in Gin Dark Mode, only the essential fields should be visible during upload:

  1. Go to Manage form display for the Media Type.
  2. Select the Template:code custom display mode.
  3. Disable all secondary fields, leaving only:
    • File/Image
    • Alternative Text / Description (The Caption)

3. Backend Relationship

The Opener passes this metadata along with the Media ID. When the asset is saved, the Drupal Media Entity holds this "Caption" as its primary metadata, ensuring the file and its description are never decoupled.


Data Synchronization: Media Metadata Mapping

To ensure the historian's caption is used as the official Title of the Digital Asset, we use a presave hook to synchronize the data.

1. Implementation Logic

Add this hook to Template:code. It automatically pulls the "Alt" text from the selected media and updates the Node's title field.

function your_module_node_presave($node) {
  if ($node->bundle() === 'digital_asset') {
    $media_id = $node->get('field_heritage_media')->target_id;
    if ($media_id) {
      $media = \Drupal\media\Entity\Media::load($media_id);
      // Retrieve the Alt text from the image source field
      $caption = $media->getSource()->getSourceFieldValue($media);
      // Sync to the node title
      $node->setTitle($caption);
    }
  }
}

2. Requirements

  • The Template:code must be populated during the selection process.
  • The Media entity must have a valid value in its source field (e.g., Alt text).