Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 4.1.x-dev

### Features

- Added [Insert](https://www.drupal.org/project/insert) module integration —
custom formatters targeting `image`, `file`, or `entity_reference` fields
are automatically exposed as Insert styles, allowing formatted output to be
inserted directly into WYSIWYG editors.

## 4.1.0-beta3 (2026-06-07)

### Features
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ Read the manual at:
- **Devel Generate** _(optional)_ — Generates sample entities with dummy
field data for the live preview system when no real entities exist with
the target field type.
- **Insert** _(optional)_ — Exposes custom formatters as
[Insert](https://www.drupal.org/project/insert) styles for `image`,
`file`, and `entity_reference` fields, allowing formatted output to be
inserted directly into WYSIWYG editors. Activate by installing the Insert
module alongside Custom Formatters; no additional configuration required.

## Roadmap

Expand All @@ -108,8 +113,6 @@ Planned features for future releases:
against Drupal coding standards directly in the formatter edit form.
- **Display Suite integration** — Format Display Suite fields with custom
formatters.
- **Insert integration** — Expose custom formatters as Insert styles for
image and file fields.
- **JSON:API integration** — Apply custom formatters to JSON:API field
output.
- **Field type management** — Change a formatter's field types after
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
},
"require-dev": {
"drupal/devel": "^5",
"drupal/insert": "^3",
"drupal/token": "^1"
},
"suggest": {
"drupal/codemirror_editor": "Provides syntax-highlighted code editing via CodeMirror for the PHP, HTML+Token, and Twig formatter engines.",
"drupal/devel": "Provides debug output options for the preview feature and Devel Generate integration for generating sample preview content.",
"drupal/field_tokens": "Provides field-level token support, useful for testing HTML+Token formatters.",
"drupal/insert": "Allows custom formatters targeting image, file, and entity_reference fields to appear as Insert styles.",
"drupal/token": "Provides token replacement support for the HTML Token formatter type."
}
}
160 changes: 160 additions & 0 deletions modules/insert.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

/**
* @file
* Insert module integration for Custom Formatters.
*
* Loaded automatically by hook_module_implements_alter() when the
* insert module is installed.
*/

declare(strict_types=1);

use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FormatterInterface;
use Drupal\custom_formatters\FormatterInterface as CustomFormattersFormatterInterface;
use Drupal\custom_formatters\InsertFieldItemList;
use Drupal\file\FileInterface;

/**
* Implements hook_insert_styles().
*/
function custom_formatters_insert_styles(string $insert_type): array {
if (!\Drupal::moduleHandler()->moduleExists('insert')) {
return [];
}

if (!in_array($insert_type, ['image', 'file'], TRUE)) {
return [];
}

$compatible_field_types = $insert_type === 'image' ? ['image'] : ['file', 'entity_reference'];

$formatters = \Drupal::entityTypeManager()
->getStorage('formatter')
->loadByProperties(['status' => TRUE]);

$styles = [];
foreach ($formatters as $formatter) {
$formatter_field_types = $formatter->get('field_types') ?? [];
if (empty(array_intersect($compatible_field_types, $formatter_field_types))) {
continue;
}

$style_name = 'custom_formatters__' . $formatter->id();
$styles[$style_name] = [
'label' => $formatter->label(),
'weight' => 0,
];
}

return $styles;
}

/**
* Implements hook_insert_render().
*/
function custom_formatters_insert_render(string $style_name, array $vars, array $insert_element): string {
if (!\Drupal::moduleHandler()->moduleExists('insert')) {
return '';
}

if (!str_starts_with($style_name, 'custom_formatters__')) {
return '';
}

$formatter_id = substr($style_name, strlen('custom_formatters__'));

$formatter = \Drupal::entityTypeManager()
->getStorage('formatter')
->load($formatter_id);

if (!$formatter instanceof CustomFormattersFormatterInterface || !$formatter->status()) {
return '';
}

$file = $vars['file'] ?? NULL;
if (!$file instanceof FileInterface) {
return '';
}

// Determine the field type based on what the formatter supports.
$formatter_field_types = $formatter->get('field_types') ?? [];
$field_type = NULL;
foreach (['image', 'file', 'entity_reference'] as $type) {
if (in_array($type, $formatter_field_types, TRUE)) {
$field_type = $type;
break;
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
if ($field_type === NULL) {
return '';
}

// Create a temporary field definition matching the formatter's field type.
$field_definition = BaseFieldDefinition::create($field_type)
->setName('_insert_file')
->setTargetEntityTypeId('file');

// Suppress alt/title for image fields in this context.
if ($field_type === 'image') {
$field_definition->setSetting('alt_field', FALSE);
$field_definition->setSetting('title_field', FALSE);
}

// Create a field item list with the file entity reference.
$items = new InsertFieldItemList(
$field_definition,
'_insert_file',
$file,
\Drupal::languageManager()->getCurrentLanguage()->getId(),
);

// Build the field item value based on the resolved field type.
$value = ['target_id' => (int) $file->id()];
if ($field_type === 'image') {
$value['alt'] = '';
$value['title'] = $file->getFilename();
}
elseif ($field_type === 'file') {
$value['display'] = 1;
$value['description'] = '';
}
$items->appendItem($value);

// Instantiate the field formatter plugin and render.
$ob_level = ob_get_level();
try {
$plugin_manager = \Drupal::service('plugin.manager.field.formatter');
$formatter_instance = $plugin_manager->createInstance(
'custom_formatters:' . $formatter_id,
[
'field_definition' => $field_definition,
'settings' => [],
'label' => 'hidden',
'view_mode' => '_custom',
'third_party_settings' => [],
],
);

\assert($formatter_instance instanceof FormatterInterface);
$elements = $formatter_instance->viewElements($items, $items->getLangcode());
if (empty($elements)) {
return '';
}

return (string) \Drupal::service('renderer')->renderInIsolation($elements);
}
catch (\Throwable $e) {
// Close any output buffers the formatter engine opened before throwing.
while (ob_get_level() > $ob_level) {
ob_end_clean();
}
\Drupal::logger('custom_formatters')->error(
'Insert render failed for formatter %id: @message',
['%id' => $formatter_id, '@message' => $e->getMessage()],
);
return '';
}
}
56 changes: 56 additions & 0 deletions src/InsertFieldItemList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Drupal\custom_formatters;

use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\file\FileInterface;

/**
* Field item list that wraps a file entity for Insert rendering.
*
* Extends EntityReferenceFieldItemList (rather than FieldItemList) so that
* formatters such as ImageFormatter that type-hint against
* EntityReferenceFieldItemListInterface receive the correct type.
*
* In Drupal 11, content entities no longer implement TypedDataInterface,
* so they cannot be passed as the $parent to FieldItemList. This subclass
* stores the file entity separately and returns it from getEntity().
*/
class InsertFieldItemList extends EntityReferenceFieldItemList {

/**
* The file entity rendered by this field item list.
*
* @var \Drupal\file\FileInterface
*/
protected FileInterface $entity;

/**
* Constructs an InsertFieldItemList.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
* The field definition.
* @param string $name
* The field name.
* @param \Drupal\file\FileInterface $file
* The file entity to render.
* @param string $langcode
* The language code.
*/
public function __construct(DataDefinitionInterface $definition, string $name, FileInterface $file, string $langcode) {
parent::__construct($definition, $name, NULL);
$this->entity = $file;
$this->langcode = $langcode;
}

/**
* {@inheritdoc}
*/
public function getEntity() {
return $this->entity;
}

}
1 change: 1 addition & 0 deletions tests/src/Functional/CustomFormattersTestBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ protected function setUp(): void {
'administer content types',
'administer custom formatters',
'administer node display',
'administer node form display',
]);

// Ensure relevant configuration present if profile isn't 'standard'.
Expand Down
Loading