Chris@0: libraryDiscovery = $library_discovery; Chris@0: $this->libraryDependencyResolver = $library_dependency_resolver; Chris@0: $this->moduleHandler = $module_handler; Chris@0: $this->themeManager = $theme_manager; Chris@0: $this->languageManager = $language_manager; Chris@0: $this->cache = $cache; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the libraries that need to be loaded. Chris@0: * Chris@0: * For example, with core/a depending on core/c and core/b on core/d: Chris@0: * @code Chris@0: * $assets = new AttachedAssets(); Chris@0: * $assets->setLibraries(['core/a', 'core/b', 'core/c']); Chris@0: * $assets->setAlreadyLoadedLibraries(['core/c']); Chris@0: * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d'] Chris@0: * @endcode Chris@0: * Chris@0: * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets Chris@0: * The assets attached to the current response. Chris@0: * Chris@0: * @return string[] Chris@0: * A list of libraries and their dependencies, in the order they should be Chris@0: * loaded, excluding any libraries that have already been loaded. Chris@0: */ Chris@0: protected function getLibrariesToLoad(AttachedAssetsInterface $assets) { Chris@0: return array_diff( Chris@0: $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()), Chris@0: $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()) Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { Chris@0: $theme_info = $this->themeManager->getActiveTheme(); Chris@0: // Add the theme name to the cache key since themes may implement Chris@0: // hook_library_info_alter(). Chris@0: $libraries_to_load = $this->getLibrariesToLoad($assets); Chris@0: $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize; Chris@0: if ($cached = $this->cache->get($cid)) { Chris@0: return $cached->data; Chris@0: } Chris@0: Chris@0: $css = []; Chris@0: $default_options = [ Chris@0: 'type' => 'file', Chris@0: 'group' => CSS_AGGREGATE_DEFAULT, Chris@0: 'weight' => 0, Chris@0: 'media' => 'all', Chris@0: 'preprocess' => TRUE, Chris@0: 'browsers' => [], Chris@0: ]; Chris@0: Chris@0: foreach ($libraries_to_load as $library) { Chris@0: list($extension, $name) = explode('/', $library, 2); Chris@0: $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); Chris@0: if (isset($definition['css'])) { Chris@0: foreach ($definition['css'] as $options) { Chris@0: $options += $default_options; Chris@0: $options['browsers'] += [ Chris@0: 'IE' => TRUE, Chris@0: '!IE' => TRUE, Chris@0: ]; Chris@0: Chris@0: // Files with a query string cannot be preprocessed. Chris@0: if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) { Chris@0: $options['preprocess'] = FALSE; Chris@0: } Chris@0: Chris@0: // Always add a tiny value to the weight, to conserve the insertion Chris@0: // order. Chris@0: $options['weight'] += count($css) / 1000; Chris@0: Chris@0: // CSS files are being keyed by the full path. Chris@0: $css[$options['data']] = $options; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Allow modules and themes to alter the CSS assets. Chris@0: $this->moduleHandler->alter('css', $css, $assets); Chris@0: $this->themeManager->alter('css', $css, $assets); Chris@0: Chris@0: // Sort CSS items, so that they appear in the correct order. Chris@0: uasort($css, 'static::sort'); Chris@0: Chris@0: // Allow themes to remove CSS files by CSS files full path and file name. Chris@0: // @todo Remove in Drupal 9.0.x. Chris@0: if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) { Chris@0: foreach ($css as $key => $options) { Chris@0: if (isset($stylesheet_remove[$key])) { Chris@0: unset($css[$key]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if ($optimize) { Chris@0: $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); Chris@0: } Chris@0: $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); Chris@0: Chris@0: return $css; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the JavaScript settings assets for this response's libraries. Chris@0: * Chris@0: * Gathers all drupalSettings from all libraries in the attached assets Chris@0: * collection and merges them. Chris@0: * Chris@0: * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets Chris@0: * The assets attached to the current response. Chris@0: * @return array Chris@0: * A (possibly optimized) collection of JavaScript assets. Chris@0: */ Chris@0: protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { Chris@0: $settings = []; Chris@0: Chris@0: foreach ($this->getLibrariesToLoad($assets) as $library) { Chris@0: list($extension, $name) = explode('/', $library, 2); Chris@0: $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); Chris@0: if (isset($definition['drupalSettings'])) { Chris@0: $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE); Chris@0: } Chris@0: } Chris@0: Chris@0: return $settings; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { Chris@0: $theme_info = $this->themeManager->getActiveTheme(); Chris@0: // Add the theme name to the cache key since themes may implement Chris@0: // hook_library_info_alter(). Additionally add the current language to Chris@0: // support translation of JavaScript files via hook_js_alter(). Chris@0: $libraries_to_load = $this->getLibrariesToLoad($assets); Chris@0: $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; Chris@0: Chris@0: if ($cached = $this->cache->get($cid)) { Chris@0: list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data; Chris@0: } Chris@0: else { Chris@0: $javascript = []; Chris@0: $default_options = [ Chris@0: 'type' => 'file', Chris@0: 'group' => JS_DEFAULT, Chris@0: 'weight' => 0, Chris@0: 'cache' => TRUE, Chris@0: 'preprocess' => TRUE, Chris@0: 'attributes' => [], Chris@0: 'version' => NULL, Chris@0: 'browsers' => [], Chris@0: ]; Chris@0: Chris@0: // Collect all libraries that contain JS assets and are in the header. Chris@0: $header_js_libraries = []; Chris@0: foreach ($libraries_to_load as $library) { Chris@0: list($extension, $name) = explode('/', $library, 2); Chris@0: $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); Chris@0: if (isset($definition['js']) && !empty($definition['header'])) { Chris@0: $header_js_libraries[] = $library; Chris@0: } Chris@0: } Chris@0: // The current list of header JS libraries are only those libraries that Chris@0: // are in the header, but their dependencies must also be loaded for them Chris@0: // to function correctly, so update the list with those. Chris@0: $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries); Chris@0: Chris@0: foreach ($libraries_to_load as $library) { Chris@0: list($extension, $name) = explode('/', $library, 2); Chris@0: $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); Chris@0: if (isset($definition['js'])) { Chris@0: foreach ($definition['js'] as $options) { Chris@0: $options += $default_options; Chris@0: Chris@0: // 'scope' is a calculated option, based on which libraries are Chris@0: // marked to be loaded from the header (see above). Chris@0: $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer'; Chris@0: Chris@0: // Preprocess can only be set if caching is enabled and no Chris@0: // attributes are set. Chris@0: $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE; Chris@0: Chris@0: // Always add a tiny value to the weight, to conserve the insertion Chris@0: // order. Chris@0: $options['weight'] += count($javascript) / 1000; Chris@0: Chris@0: // Local and external files must keep their name as the associative Chris@0: // key so the same JavaScript file is not added twice. Chris@0: $javascript[$options['data']] = $options; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Allow modules and themes to alter the JavaScript assets. Chris@0: $this->moduleHandler->alter('js', $javascript, $assets); Chris@0: $this->themeManager->alter('js', $javascript, $assets); Chris@0: Chris@0: // Sort JavaScript assets, so that they appear in the correct order. Chris@0: uasort($javascript, 'static::sort'); Chris@0: Chris@0: // Prepare the return value: filter JavaScript assets per scope. Chris@0: $js_assets_header = []; Chris@0: $js_assets_footer = []; Chris@0: foreach ($javascript as $key => $item) { Chris@0: if ($item['scope'] == 'header') { Chris@0: $js_assets_header[$key] = $item; Chris@0: } Chris@0: elseif ($item['scope'] == 'footer') { Chris@0: $js_assets_footer[$key] = $item; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($optimize) { Chris@0: $collection_optimizer = \Drupal::service('asset.js.collection_optimizer'); Chris@0: $js_assets_header = $collection_optimizer->optimize($js_assets_header); Chris@0: $js_assets_footer = $collection_optimizer->optimize($js_assets_footer); Chris@0: } Chris@0: Chris@0: // If the core/drupalSettings library is being loaded or is already Chris@0: // loaded, get the JavaScript settings assets, and convert them into a Chris@0: // single "regular" JavaScript asset. Chris@0: $libraries_to_load = $this->getLibrariesToLoad($assets); Chris@0: $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); Chris@0: $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0; Chris@0: Chris@0: // Initialize settings to FALSE since they are not needed by default. This Chris@0: // distinguishes between an empty array which must still allow Chris@0: // hook_js_settings_alter() to be run. Chris@0: $settings = FALSE; Chris@0: if ($settings_required && $settings_have_changed) { Chris@0: $settings = $this->getJsSettingsAssets($assets); Chris@0: // Allow modules to add cached JavaScript settings. Chris@0: foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) { Chris@18: $function = $module . '_js_settings_build'; Chris@0: $function($settings, $assets); Chris@0: } Chris@0: } Chris@0: $settings_in_header = in_array('core/drupalSettings', $header_js_libraries); Chris@0: $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']); Chris@0: } Chris@0: Chris@0: if ($settings !== FALSE) { Chris@0: // Attached settings override both library definitions and Chris@0: // hook_js_settings_build(). Chris@0: $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); Chris@0: // Allow modules and themes to alter the JavaScript settings. Chris@0: $this->moduleHandler->alter('js_settings', $settings, $assets); Chris@0: $this->themeManager->alter('js_settings', $settings, $assets); Chris@0: // Update the $assets object accordingly, so that it reflects the final Chris@0: // settings. Chris@0: $assets->setSettings($settings); Chris@0: $settings_as_inline_javascript = [ Chris@0: 'type' => 'setting', Chris@0: 'group' => JS_SETTING, Chris@0: 'weight' => 0, Chris@0: 'browsers' => [], Chris@0: 'data' => $settings, Chris@0: ]; Chris@0: $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript]; Chris@0: // Prepend to the list of JS assets, to render it first. Preferably in Chris@0: // the footer, but in the header if necessary. Chris@0: if ($settings_in_header) { Chris@0: $js_assets_header = $settings_js_asset + $js_assets_header; Chris@0: } Chris@0: else { Chris@0: $js_assets_footer = $settings_js_asset + $js_assets_footer; Chris@0: } Chris@0: } Chris@0: return [ Chris@0: $js_assets_header, Chris@0: $js_assets_footer, Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sorts CSS and JavaScript resources. Chris@0: * Chris@0: * This sort order helps optimize front-end performance while providing Chris@0: * modules and themes with the necessary control for ordering the CSS and Chris@0: * JavaScript appearing on a page. Chris@0: * Chris@0: * @param $a Chris@0: * First item for comparison. The compared items should be associative Chris@0: * arrays of member items. Chris@0: * @param $b Chris@0: * Second item for comparison. Chris@0: * Chris@0: * @return int Chris@0: */ Chris@0: public static function sort($a, $b) { Chris@0: // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT Chris@0: // group appear before items in the CSS_AGGREGATE_THEME group. Modules may Chris@0: // create additional groups by defining their own constants. Chris@0: if ($a['group'] < $b['group']) { Chris@0: return -1; Chris@0: } Chris@0: elseif ($a['group'] > $b['group']) { Chris@0: return 1; Chris@0: } Chris@0: // Finally, order by weight. Chris@0: elseif ($a['weight'] < $b['weight']) { Chris@0: return -1; Chris@0: } Chris@0: elseif ($a['weight'] > $b['weight']) { Chris@0: return 1; Chris@0: } Chris@0: else { Chris@0: return 0; Chris@0: } Chris@0: } Chris@0: Chris@0: }