3 namespace Drupal\blazy;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Entity\EntityInterface;
9 * Implements a public facing blazy manager.
11 * A few modules re-use this: GridStack, Mason, Slick...
13 class BlazyManager extends BlazyManagerBase {
16 * Cleans up empty breakpoints.
18 * @param array $settings
19 * The settings being modified.
21 public function cleanUpBreakpoints(array &$settings = []) {
22 if (!empty($settings['breakpoints'])) {
23 foreach ($settings['breakpoints'] as $key => &$breakpoint) {
24 $breakpoint = array_filter($breakpoint);
26 if (empty($breakpoint['width']) && empty($breakpoint['image_style'])) {
27 unset($settings['breakpoints'][$key]);
32 // Identify that Blazy can be activated only by breakpoints.
33 if (empty($settings['blazy'])) {
34 $settings['blazy'] = !empty($settings['breakpoints']);
39 * Checks if an image style contains crop effect.
41 public function isCrop($style = NULL) {
42 foreach ($style->getEffects() as $effect) {
43 if (strpos($effect->getPluginId(), 'crop') !== FALSE) {
52 * Sets dimensions once to reduce method calls, if image style contains crop.
54 * The implementor should only call this if not using Responsive image style.
56 * @param array $settings
57 * The settings being modified.
59 public function setDimensionsOnce(array &$settings = []) {
60 $item = isset($settings['item']) ? $settings['item'] : NULL;
61 $dimensions['width'] = $settings['original_width'] = isset($item->width) ? $item->width : NULL;
62 $dimensions['height'] = $settings['original_height'] = isset($item->height) ? $item->height : NULL;
64 // If image style contains crop, sets dimension once, and let all inherit.
65 if (!empty($settings['image_style']) && ($style = $this->entityLoad($settings['image_style']))) {
66 if ($this->isCrop($style)) {
67 $style->transformDimensions($dimensions, $settings['uri']);
69 $settings['height'] = $dimensions['height'];
70 $settings['width'] = $dimensions['width'];
72 // Informs individual images that dimensions are already set once.
73 $settings['_dimensions'] = TRUE;
77 // Also sets breakpoint dimensions once, if cropped.
78 if (!empty($settings['breakpoints'])) {
79 $this->buildDataBlazy($settings, $item);
82 // Remove these since this method is meant for top-level container.
83 unset($settings['uri'], $settings['item']);
87 * Checks for Blazy formatter such as from within a Views style plugin.
89 * Ensures the settings traverse up to the container where Blazy is clueless.
90 * The supported plugins can add [data-blazy] attribute into its container
91 * containing $settings['blazy_data'] converted into [data-blazy] JSON.
93 * @param array $settings
94 * The settings being modified.
96 * The item containing settings or item keys.
98 public function isBlazy(array &$settings, array $item = []) {
99 // Retrieves Blazy formatter related settings from within Views style.
100 $content = !empty($settings['item_id']) && isset($item[$settings['item_id']]) ? $item[$settings['item_id']] : $item;
102 // 1. Blazy formatter within Views fields by supported modules.
103 if (isset($item['settings'])) {
104 // Prevents edge case with unexpected flattened Views results which is
105 // normally triggered by checking "Use field template" option.
106 $blazy = is_array($content) && isset($content['#build']['settings']) ? $content['#build']['settings'] : [];
108 // Allows breakpoints overrides such as multi-styled images by GridStack.
109 if (empty($settings['breakpoints']) && isset($blazy['breakpoints'])) {
110 $settings['breakpoints'] = $blazy['breakpoints'];
123 foreach ($cherries as $key) {
124 $fallback = isset($settings[$key]) ? $settings[$key] : '';
125 $settings[$key] = isset($blazy[$key]) && empty($fallback) ? $blazy[$key] : $fallback;
129 // 2. Blazy Views fields by supported modules.
130 if (is_array($content) && isset($content['#view']) && ($view = $content['#view'])) {
131 if ($blazy_field = BlazyViews::viewsField($view)) {
132 $settings = array_merge(array_filter($blazy_field->mergedViewsSettings()), array_filter($settings));
136 // Provides data for the [data-blazy] attribute at the containing element.
137 $this->cleanUpBreakpoints($settings);
138 if (!empty($settings['breakpoints'])) {
139 $image = isset($item['item']) ? $item['item'] : NULL;
140 $this->buildDataBlazy($settings, $image);
142 unset($settings['uri']);
146 * Builds breakpoints suitable for top-level [data-blazy] wrapper attributes.
148 * The hustle is because we need to define dimensions once, if applicable, and
149 * let all images inherit. Each breakpoint image may be cropped, or scaled
150 * without a crop. To set dimensions once requires all breakpoint images
151 * uniformly cropped. But that is not always the case.
153 * @param array $settings
154 * The settings being modified.
155 * @param object|mixed $item
156 * The \Drupal\image\Plugin\Field\FieldType\ImageItem item, or array when
157 * dealing with Video Embed Field.
159 * @todo: Refine this like everything else.
161 public function buildDataBlazy(array &$settings, $item = NULL) {
162 // Early opt-out if blazy_data has already been defined.
163 // Blazy doesn't always deal with image directly.
164 if (!empty($settings['blazy_data'])) {
168 if (empty($settings['original_width'])) {
169 $settings['original_width'] = isset($item->width) ? $item->width : NULL;
170 $settings['original_height'] = isset($item->height) ? $item->height : NULL;
173 $json = $sources = [];
174 $end = end($settings['breakpoints']);
175 foreach ($settings['breakpoints'] as $key => $breakpoint) {
176 if (empty($breakpoint['image_style']) || empty($breakpoint['width'])) {
180 if ($width = Blazy::widthFromDescriptors($breakpoint['width'])) {
181 // If contains crop, sets dimension once, and let all images inherit.
182 if (!empty($settings['uri']) && !empty($settings['ratio'])) {
183 $dimensions['width'] = $settings['original_width'];
184 $dimensions['height'] = $settings['original_height'];
186 if (!empty($breakpoint['image_style']) && ($style = $this->entityLoad($breakpoint['image_style']))) {
187 if ($this->isCrop($style)) {
188 $style->transformDimensions($dimensions, $settings['uri']);
189 $padding = round((($dimensions['height'] / $dimensions['width']) * 100), 2);
190 $json['dimensions'][$width] = $padding;
192 // Only set padding-bottom for the last breakpoint to avoid FOUC.
193 if ($end['width'] == $breakpoint['width']) {
194 $settings['padding_bottom'] = $padding;
200 // If BG, provide [data-src-BREAKPOINT].
201 if (!empty($settings['background'])) {
202 $sources[] = ['width' => (int) $width, 'src' => 'data-src-' . $key];
207 // As of Blazy v1.6.0 applied to BG only.
209 $json['breakpoints'] = $sources;
212 // @todo: A more efficient way not to do this in the first place.
213 // ATM, this is okay as this method is run once on the top-level container.
214 if (isset($json['dimensions']) && (count($settings['breakpoints']) != count($json['dimensions']))) {
215 unset($json['dimensions'], $settings['padding_bottom']);
218 // Supported modules can add blazy_data as [data-blazy] to the container.
219 // This also informs individual images to not work with dimensions any more
220 // if the image style contains 'crop'.
222 $settings['blazy_data'] = $json;
225 // Identify that Blazy can be activated only by breakpoints.
226 $settings['blazy'] = TRUE;
230 * Returns the enforced content, or image using theme_blazy().
232 * @param array $build
233 * The array containing: item, content, settings, or optional captions.
236 * The alterable and renderable array of enforced content, or theme_blazy().
238 public function getImage(array $build = []) {
239 if (empty($build['item'])) {
243 /** @var Drupal\image\Plugin\Field\FieldType\ImageItem $item */
244 $item = $build['item'];
245 $settings = &$build['settings'];
246 $settings['delta'] = isset($settings['delta']) ? $settings['delta'] : 0;
247 $settings['image_style'] = isset($settings['image_style']) ? $settings['image_style'] : '';
249 if (empty($settings['uri'])) {
250 $settings['uri'] = ($entity = $item->entity) && empty($item->uri) ? $entity->getFileUri() : $item->uri;
253 // Respects content not handled by theme_blazy(), but passed through.
254 if (empty($build['content'])) {
256 '#theme' => isset($settings['theme_hook_image']) ? $settings['theme_hook_image'] : 'blazy',
257 '#delta' => $settings['delta'],
259 '#image_style' => $settings['image_style'],
261 '#pre_render' => [[$this, 'preRenderImage']],
265 $image = $build['content'];
268 $this->getModuleHandler()->alter('blazy', $image, $settings);
274 * Builds the Blazy image as a structured array ready for ::renderer().
276 * @param array $element
277 * The pre-rendered element.
280 * The renderable array of pre-rendered element.
282 public function preRenderImage(array $element) {
283 $build = $element['#build'];
284 $item = $build['item'];
285 unset($element['#build']);
287 $settings = $build['settings'];
292 // Extract field item attributes for the theme function, and unset them
293 // from the $item so that the field template does not re-render them.
294 $item_attributes = [];
295 if (isset($item->_attributes)) {
296 $item_attributes = $item->_attributes;
297 unset($item->_attributes);
300 // Responsive image integration.
301 $settings['responsive_image_style_id'] = '';
302 if (!empty($settings['resimage']) && !empty($settings['responsive_image_style'])) {
303 $responsive_image_style = $this->entityLoad($settings['responsive_image_style'], 'responsive_image_style');
304 $settings['lazy'] = '';
305 if (!empty($responsive_image_style)) {
306 $settings['responsive_image_style_id'] = $responsive_image_style->id();
307 if ($this->configLoad('responsive_image')) {
308 $item_attributes['data-srcset'] = TRUE;
309 $settings['lazy'] = 'responsive';
311 $element['#cache']['tags'] = $this->getResponsiveImageCacheTags($responsive_image_style);
315 if (!isset($settings['_no_cache'])) {
316 $file_tags = isset($settings['file_tags']) ? $settings['file_tags'] : [];
317 $settings['cache_tags'] = empty($settings['cache_tags']) ? $file_tags : Cache::mergeTags($settings['cache_tags'], $file_tags);
319 $element['#cache']['max-age'] = -1;
320 foreach (['contexts', 'keys', 'tags'] as $key) {
321 if (!empty($settings['cache_' . $key])) {
322 $element['#cache'][$key] = $settings['cache_' . $key];
328 $element['#item'] = $item;
329 $element['#captions'] = empty($build['captions']) ? [] : ['inline' => $build['captions']];
330 $element['#item_attributes'] = $item_attributes;
331 $element['#url'] = '';
332 $element['#settings'] = $settings;
334 foreach (['caption', 'media', 'wrapper'] as $key) {
335 if (!empty($settings[$key . '_attributes'])) {
336 $element["#$key" . '_attributes'] = $settings[$key . '_attributes'];
340 if (!empty($settings['media_switch']) && $settings['media_switch'] != 'media') {
341 if ($settings['media_switch'] == 'content' && !empty($settings['content_url'])) {
342 $element['#url'] = $settings['content_url'];
344 elseif (!empty($settings['lightbox'])) {
345 BlazyLightbox::build($element);
353 * Returns the entity view, if available.
355 * @param object $entity
356 * The entity being rendered.
357 * @param array $settings
358 * The settings containing view_mode.
359 * @param string $fallback
360 * The fallback content when all fails, probably just entity label.
363 * The renderable array of the view builder, or false if not applicable.
365 public function getEntityView($entity = NULL, array $settings = [], $fallback = '') {
366 if ($entity instanceof EntityInterface) {
367 $entity_type_id = $entity->getEntityTypeId();
368 $view_hook = $entity_type_id . '_view';
369 $view_mode = empty($settings['view_mode']) ? 'default' : $settings['view_mode'];
370 $langcode = $entity->language()->getId();
372 // If module implements own {entity_type}_view.
373 if (function_exists($view_hook)) {
374 return $view_hook($entity, $view_mode, $langcode);
376 // If entity has view_builder handler.
377 elseif ($this->getEntityTypeManager()->hasHandler($entity_type_id, 'view_builder')) {
378 return $this->getEntityTypeManager()->getViewBuilder($entity_type_id)->view($entity, $view_mode, $langcode);
381 return ['#markup' => $fallback];
389 * Returns the Responsive image cache tags.
391 * @param object $responsive
392 * The responsive image style entity.
395 * The responsive image cache tags, or empty array.
397 public function getResponsiveImageCacheTags($responsive = NULL) {
399 $image_styles_to_load = [];
401 $cache_tags = Cache::mergeTags($cache_tags, $responsive->getCacheTags());
402 $image_styles_to_load = $responsive->getImageStyleIds();
405 $image_styles = $this->entityLoadMultiple('image_style', $image_styles_to_load);
406 foreach ($image_styles as $image_style) {
407 $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags());