Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix handling for CSS and JS attachments #139

Merged
merged 6 commits into from
Feb 4, 2024
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
53 changes: 53 additions & 0 deletions src/Asset/RetrofitJsCollectionRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Retrofit\Drupal\Asset;

use Drupal\Core\Asset\AssetCollectionRendererInterface;

class RetrofitJsCollectionRenderer implements AssetCollectionRendererInterface
{
/**
* @var array<array{
* '#type': string,
* '#tag': string,
* '#value'?: string,
* '#attributes'?: mixed[],
* }>
*/
protected array $retrofitFooter = [];

public function __construct(
private readonly AssetCollectionRendererInterface $inner,
) {
}

/**
* @param mixed[] $assets
* @return mixed[]
*/
public function render(array $assets): array
{
$is_footer = isset($assets['retrofit']);
unset($assets['retrofit']);
$elements = $this->inner->render($assets);
if ($is_footer && !empty($this->retrofitFooter)) {
$elements = array_merge($elements, $this->retrofitFooter);
}
return $elements;
}

/**
* @param array{
* '#type': string,
* '#tag': string,
* '#value'?: string,
* '#attributes'?: mixed[],
* } $element
*/
public function addRetrofitFooter(array $element): void
{
$this->retrofitFooter[] = $element;
}
}
106 changes: 106 additions & 0 deletions src/Asset/RetrofitLibraryDiscovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace Retrofit\Drupal\Asset;

use Drupal\Core\Asset\LibraryDiscoveryInterface;

class RetrofitLibraryDiscovery implements LibraryDiscoveryInterface
{
/**
* @var array<mixed[]>
*/
protected array $retrofitLibraries = [];

public function __construct(
private readonly LibraryDiscoveryInterface $inner,
) {
}

/**
* @param string $extension
* @return array<mixed[]>
*/
public function getLibrariesByExtension($extension): array
{
return match ($extension) {
'retrofit' => $this->retrofitLibraries,
default => $this->inner->getLibrariesByExtension($extension),
};
}

/**
* @param string $extension
* @param string $name
* @return mixed[]|false
*/
public function getLibraryByName($extension, $name): array|false
{
return match ($extension) {
'retrofit' => $this->retrofitLibraries[$name] ?? false,
default => $this->inner->getLibraryByName($extension, $name),
};
}

public function clearCachedDefinitions(): void
{
$this->inner->clearCachedDefinitions();
}

/**
* @param array{
* css?: mixed[],
* js?: mixed[],
* requires_jquery?: bool,
* } $attachments
*/
public function setRetrofitLibrary(string $key, array $attachments): void
{
$this->retrofitLibraries[$key]['license'] = [];
if (!empty($attachments['js'])) {
$this->retrofitLibraries[$key]['dependencies'][] = 'core/drupalSettings';
$this->retrofitLibraries[$key]['js'][] = [
'data' => 'retrofit',
'scope' => 'footer',
];
if (!empty($attachments['requires_jquery'])) {
$this->retrofitLibraries[$key]['dependencies'][] = 'core/jquery';
$this->retrofitLibraries[$key]['dependencies'][] = 'core/once';
}
}
foreach (['css', 'js'] as $type) {
foreach ($attachments[$type] ?? [] as $data => $options) {
if (!is_array($options)) {
$options = ['data' => $options];
}
if (!is_numeric($data)) {
$options['data'] = $data;
}
$options += [
'type' => 'file',
'version' => -1,
];
switch ($type) {
case 'css':
$options['weight'] ??= 0;
$options['weight'] += match ($options['group'] ?? CSS_DEFAULT) {
CSS_SYSTEM => CSS_LAYOUT,
CSS_THEME, 100 => CSS_AGGREGATE_THEME,
default => CSS_AGGREGATE_DEFAULT,
};
if (!isset($options['group']) || $options['group'] !== CSS_AGGREGATE_THEME) {
$options['group'] = CSS_AGGREGATE_DEFAULT;
}
break;

case 'js':
$options['group'] = JS_LIBRARY;
$options['minified'] ??= false;
break;
}
$this->retrofitLibraries[$key][$type][] = $options;
}
}
}
}
14 changes: 13 additions & 1 deletion src/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Drupal\Core\Template\Loader\FilesystemLoader;
use Retrofit\Drupal\Asset\RetrofitJsCollectionRenderer;
use Retrofit\Drupal\Asset\RetrofitLibraryDiscovery;
use Retrofit\Drupal\Controller\RetrofitTitleResolver;
use Retrofit\Drupal\Field\FieldTypePluginManager;
use Retrofit\Drupal\Form\FormBuilder;
Expand Down Expand Up @@ -112,11 +114,21 @@ public function register(ContainerBuilder $container)

$container->register(RetrofitHtmlResponseAttachmentsProcessor::class)
->setDecoratedService('html_response.attachments_processor')
->addArgument(new Reference(RetrofitHtmlResponseAttachmentsProcessor::class . '.inner'));
->addArgument(new Reference(RetrofitHtmlResponseAttachmentsProcessor::class . '.inner'))
->addArgument(new Reference('asset.js.collection_renderer'))
->setAutowired(true);

$container->register(AttachmentResponseSubscriber::class)
->addTag('event_subscriber');

$container->register(RetrofitLibraryDiscovery::class)
->setDecoratedService('library.discovery')
->setAutowired(true);

$container->register(RetrofitJsCollectionRenderer::class)
->setDecoratedService('asset.js.collection_renderer')
->setAutowired(true);

$container->setDefinition(
FieldTypePluginManager::class,
(new ChildDefinition('plugin.manager.field.field_type'))
Expand Down
116 changes: 107 additions & 9 deletions src/Render/RetrofitHtmlResponseAttachmentsProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@

namespace Retrofit\Drupal\Render;

use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
use Retrofit\Drupal\Asset\RetrofitJsCollectionRenderer;
use Retrofit\Drupal\Asset\RetrofitLibraryDiscovery;

final class RetrofitHtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface
{
public function __construct(
private readonly AttachmentsResponseProcessorInterface $inner,
private readonly AssetCollectionRendererInterface $jsCollectionRenderer,
private readonly LibraryDiscoveryInterface $libraryDiscovery,
) {
}

Expand All @@ -29,19 +36,110 @@ public function processAttachments(AttachmentsInterface $response)
}
}
}
if (isset($attachments['js']) && is_array($attachments['js'])) {
foreach ($attachments['js'] as $key => $item) {
if (is_array($item) && isset($item['type'], $item['data']) && $item['type'] === 'setting') {
$attachments['drupalSettings'] = NestedArray::mergeDeepArray(
[$attachments['drupalSettings'] ?? [], $item['data']],
true,
$retrofit_library = [];
if (isset($attachments['css']) && is_array($attachments['css'])) {
foreach ($attachments['css'] as $key => $item) {
if (is_array($item) && isset($item['type'], $item['data']) && $item['type'] === 'inline') {
$element = [
'#tag' => 'style',
'#value' => $item['data'],
'#weight' => $item['weight'] ?? 0,
];
unset(
$item['data'],
$item['type'],
$item['basename'],
$item['group'],
$item['every_page'],
$item['weight'],
$item['preprocess'],
$item['browsers'],
);
unset($attachments['js'][$key]);
$element['#attributes'] = $item;
$attachments['html_head'][] = [
$element,
"retrofit:$key",
];
unset($attachments['css'][$key]);
}
}
$retrofit_library['css'] = $attachments['css'];
unset($attachments['css']);
asort($retrofit_library['css']);
}
if (isset($attachments['js']) && is_array($attachments['js'])) {
$requires_jquery = false;
foreach ($attachments['js'] as $key => &$item) {
if (is_array($item)) {
$requires_jquery = !empty($item['requires_jquery']) ?: $requires_jquery;
switch ($item['type'] ?? 'file') {
case 'inline':
$element = [
'#tag' => 'script',
'#value' => $item['data'] ?? $key,
'#weight' => $item['weight'] ?? 0,
];
$scope = $item['scope'] ?? 'footer';
unset(
$item['data'],
$item['type'],
$item['scope'],
$item['group'],
$item['every_page'],
$item['weight'],
$item['requires_jquery'],
$item['cache'],
$item['preprocess'],
);
$element['#attributes'] = $item;
unset($attachments['js'][$key]);
switch ($scope) {
case 'header':
$attachments['html_head'][] = [
$element,
"retrofit:$key",
];
break;

default:
$element['#type'] = 'html_tag';
assert($this->jsCollectionRenderer instanceof RetrofitJsCollectionRenderer);
$this->jsCollectionRenderer->addRetrofitFooter($element);
break;
}
break;

case 'setting':
if (isset($item['data'])) {
$attachments['drupalSettings'] = NestedArray::mergeDeepArray(
[$attachments['drupalSettings'] ?? [], $item['data']],
true,
);
}
unset($attachments['js'][$key]);
break;

default:
$item['attributes']['defer'] = $item['defer'] ?? false;
break;
}
}
}
assert($this->jsCollectionRenderer instanceof RetrofitJsCollectionRenderer);
$this->jsCollectionRenderer->addRetrofitFooter([
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => 'Drupal.settings = drupalSettings;',
]);
$retrofit_library['requires_jquery'] = $requires_jquery;
$retrofit_library['js'] = $attachments['js'];
unset($attachments['js']);
asort($retrofit_library['js']);
}
// @todo log these removals?
unset($attachments['css'], $attachments['js']);
$name = Crypt::hashBase64(serialize($retrofit_library));
assert($this->libraryDiscovery instanceof RetrofitLibraryDiscovery);
$this->libraryDiscovery->setRetrofitLibrary($name, $retrofit_library);
$attachments['library'][] = "retrofit/$name";
$response->setAttachments($attachments);
return $this->inner->processAttachments($response);
}
Expand Down
2 changes: 2 additions & 0 deletions src/constants/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@

// In Drupal 10 this has been replaced with CSS_AGGREGATE_DEFAULT.
const CSS_DEFAULT = 0;

const CSS_SYSTEM = -100;
30 changes: 17 additions & 13 deletions src/functions/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,34 +214,38 @@ function drupal_add_library(string $module, string $name, ?bool $every_page = nu
*/
function drupal_add_js(array|string|null $data = null, array|string|null $options = null): array
{
if ($data === null) {
return [];
}

$attachment_subscriber = \Drupal::getContainer()->get(AttachmentResponseSubscriber::class);
assert($attachment_subscriber instanceof AttachmentResponseSubscriber);

if (is_string($options)) {
$options ??= [];
if (!is_array($options)) {
$options = ['type' => $options];
} elseif ($options === null) {
$options = [];
}

$type = $options['type'] ?? 'file';
switch ($type) {
$options += [
'type' => 'file',
'requires_jquery' => isset($options['type']) && $options['type'] === 'setting',
'scope' => 'footer',
'cache' => true,
'defer' => false,
'preprocess' => true,
'data' => $data,
];
$options['preprocess'] = $options['cache'] ? $options['preprocess'] : false;
switch ($options['type']) {
case 'setting':
if (is_array($data)) {
$attachment_subscriber->addAttachments([
'drupalSettings' => $data,
]);
} else {
// @todo log warning if string? Cannot discern what D7 did.
}

break;

case 'inline':
$attachment_subscriber->addAttachments([
'js' => $options,
'js' => [
$options,
],
]);
break;

Expand Down
Loading
Loading