Version 1
[yaffs-website] / web / modules / contrib / blazy / src / BlazyManager.php
1 <?php
2
3 namespace Drupal\blazy;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\image\Entity\ImageStyle;
8
9 /**
10  * Implements a public facing blazy manager.
11  *
12  * A few modules re-use this: GridStack, Mason, Slick...
13  */
14 class BlazyManager extends BlazyManagerBase {
15
16   /**
17    * Cleans up empty breakpoints.
18    *
19    * @param array $settings
20    *   The settings being modified.
21    */
22   public function cleanUpBreakpoints(array &$settings = []) {
23     if (empty($settings['breakpoints'])) {
24       return;
25     }
26
27     foreach ($settings['breakpoints'] as $key => &$breakpoint) {
28       $breakpoint = array_filter($breakpoint);
29       if (empty($breakpoint['width']) && empty($breakpoint['image_style'])) {
30         unset($settings['breakpoints'][$key]);
31       }
32     }
33
34     // Identify that Blazy can be activated only by breakpoints.
35     if (empty($settings['blazy'])) {
36       $settings['blazy'] = !empty($settings['breakpoints']);
37     }
38   }
39
40   /**
41    * Sets dimensions once to reduce method calls, if image style contains crop.
42    *
43    * The implementor should only call this if not using Responsive image style.
44    *
45    * @param array $settings
46    *   The settings being modified.
47    */
48   public function setDimensionsOnce(array &$settings = []) {
49     $item                 = isset($settings['item']) ? $settings['item'] : NULL;
50     $dimensions['width']  = $settings['original_width'] = isset($item->width) ? $item->width : NULL;
51     $dimensions['height'] = $settings['original_height'] = isset($item->height) ? $item->height : NULL;
52
53     // If image style contains crop, sets dimension once, and let all inherit.
54     if (($style = ImageStyle::load($settings['image_style'])) && Blazy::isCrop($style)) {
55       $style->transformDimensions($dimensions, $settings['uri']);
56
57       $settings['height'] = $dimensions['height'];
58       $settings['width']  = $dimensions['width'];
59
60       // Informs individual images that dimensions are already set once.
61       $settings['_dimensions'] = TRUE;
62     }
63
64     // Also sets breakpoint dimensions once, if cropped.
65     if (!empty($settings['breakpoints'])) {
66       $this->buildDataBlazy($settings, $item);
67     }
68
69     // Remove these since this method is meant for top-level container.
70     unset($settings['uri'], $settings['item']);
71   }
72
73   /**
74    * Checks for Blazy formatter such as from within a Views style plugin.
75    *
76    * Ensures the settings traverse up to the container where Blazy is clueless.
77    * The supported plugins can add [data-blazy] attribute into its container
78    * containing $settings['blazy_data'] converted into [data-blazy] JSON.
79    *
80    * @param array $settings
81    *   The settings being modified.
82    * @param array $item
83    *   The item containing settings or item keys.
84    */
85   public function isBlazy(array &$settings, array $item = []) {
86     // Retrieves Blazy formatter related settings from within Views style.
87     $item_id  = $settings['item_id'];
88     $content  = isset($item[$item_id]) ? $item[$item_id] : $item;
89     $cherries = [
90       'blazy',
91       'box_style',
92       'image_style',
93       'lazy',
94       'media_switch',
95       'ratio',
96       'uri',
97     ];
98
99     // 1. Blazy formatter within Views fields by supported modules.
100     if (isset($item['settings'])) {
101       $blazy = isset($content['#build']['settings']) ? $content['#build']['settings'] : [];
102
103       // Allows breakpoints overrides such as multi-styled images by GridStack.
104       if (empty($settings['breakpoints']) && isset($blazy['breakpoints'])) {
105         $settings['breakpoints'] = $blazy['breakpoints'];
106       }
107
108       foreach ($cherries as $key) {
109         $fallback = isset($settings[$key]) ? $settings[$key] : '';
110         $settings[$key] = isset($blazy[$key]) && empty($fallback) ? $blazy[$key] : $fallback;
111       }
112     }
113
114     // 2. Blazy Views fields by supported modules.
115     if (isset($content['#view']) && ($view = $content['#view'])) {
116       if ($blazy_field = BlazyViews::viewsField($view)) {
117         $settings = array_merge(array_filter($blazy_field->mergedViewsSettings()), array_filter($settings));
118       }
119     }
120
121     // Provides data for the [data-blazy] attribute at the containing element.
122     $this->cleanUpBreakpoints($settings);
123     if (!empty($settings['breakpoints'])) {
124       $image = isset($item['item']) ? $item['item'] : NULL;
125       $this->buildDataBlazy($settings, $image);
126     }
127     unset($settings['uri']);
128   }
129
130   /**
131    * Builds breakpoints suitable for top-level [data-blazy] wrapper attributes.
132    *
133    * The hustle is because we need to define dimensions once, if applicable, and
134    * let all images inherit. Each breakpoint image may be cropped, or scaled
135    * without a crop. To set dimensions once requires all breakpoint images
136    * uniformly cropped. But that is not always the case.
137    *
138    * @param array $settings
139    *   The settings being modified.
140    * @param object|mixed $item
141    *   The \Drupal\image\Plugin\Field\FieldType\ImageItem item, or array when
142    *   dealing with Video Embed Field.
143    *
144    * @todo: Refine this like everything else.
145    */
146   public function buildDataBlazy(array &$settings, $item = NULL) {
147     // Early opt-out if blazy_data has already been defined.
148     // Blazy doesn't always deal with image directly.
149     if (!empty($settings['blazy_data'])) {
150       return;
151     }
152
153     if (empty($settings['original_width'])) {
154       $settings['original_width'] = isset($item->width) ? $item->width : NULL;
155       $settings['original_height'] = isset($item->height) ? $item->height : NULL;
156     }
157
158     $json = $sources = [];
159     $end = end($settings['breakpoints']);
160     foreach ($settings['breakpoints'] as $key => $breakpoint) {
161       if (empty($breakpoint['image_style']) || empty($breakpoint['width'])) {
162         continue;
163       }
164
165       if ($width = Blazy::widthFromDescriptors($breakpoint['width'])) {
166         // If contains crop, sets dimension once, and let all images inherit.
167         if (!empty($settings['uri']) && !empty($settings['ratio'])) {
168           $dimensions['width'] = $settings['original_width'];
169           $dimensions['height'] = $settings['original_height'];
170
171           if (($style = ImageStyle::load($breakpoint['image_style'])) && Blazy::isCrop($style)) {
172             $style->transformDimensions($dimensions, $settings['uri']);
173             $padding = round((($dimensions['height'] / $dimensions['width']) * 100), 2);
174             $json['dimensions'][$width] = $padding;
175
176             // Only set padding-bottom for the last breakpoint to avoid FOUC.
177             if ($end['width'] == $breakpoint['width']) {
178               $settings['padding_bottom'] = $padding;
179             }
180           }
181         }
182
183         // If BG, provide [data-src-BREAKPOINT].
184         if (!empty($settings['background'])) {
185           $sources[] = ['width' => (int) $width, 'src' => 'data-src-' . $key];
186         }
187       }
188     }
189
190     // As of Blazy v1.6.0 applied to BG only.
191     if ($sources) {
192       $json['breakpoints'] = $sources;
193     }
194
195     // @todo: A more efficient way not to do this in the first place.
196     // ATM, this is okay as this method is run once on the top-level container.
197     if (isset($json['dimensions']) && (count($settings['breakpoints']) != count($json['dimensions']))) {
198       unset($json['dimensions'], $settings['padding_bottom']);
199     }
200
201     // Supported modules can add blazy_data as [data-blazy] to the container.
202     // This also informs individual images to not work with dimensions any more
203     // if the image style contains 'crop'.
204     if ($json) {
205       $settings['blazy_data'] = $json;
206     }
207
208     // Identify that Blazy can be activated only by breakpoints.
209     $settings['blazy'] = TRUE;
210   }
211
212   /**
213    * Returns the enforced content, or image using theme_blazy().
214    *
215    * @param array $build
216    *   The array containing: item, content, settings, or optional captions.
217    *
218    * @return array
219    *   The alterable and renderable array of enforced content, or theme_blazy().
220    */
221   public function getImage(array $build = []) {
222     /** @var Drupal\image\Plugin\Field\FieldType\ImageItem $item */
223     $item     = $build['item'];
224     $settings = &$build['settings'];
225     $theme    = isset($settings['theme_hook_image']) ? $settings['theme_hook_image'] : 'blazy';
226
227     if (empty($item)) {
228       return [];
229     }
230
231     $settings['delta']       = isset($settings['delta']) ? $settings['delta'] : 0;
232     $settings['image_style'] = isset($settings['image_style']) ? $settings['image_style'] : '';
233
234     if (empty($settings['uri'])) {
235       $settings['uri'] = ($entity = $item->entity) && empty($item->uri) ? $entity->getFileUri() : $item->uri;
236     }
237
238     // Respects content not handled by theme_blazy(), but passed through.
239     if (empty($build['content'])) {
240       $image = [
241         '#theme'       => $theme,
242         '#delta'       => $settings['delta'],
243         '#item'        => [],
244         '#image_style' => $settings['image_style'],
245         '#build'       => $build,
246         '#pre_render'  => [[$this, 'preRenderImage']],
247       ];
248     }
249     else {
250       $image = $build['content'];
251     }
252
253     $this->getModuleHandler()->alter('blazy', $image, $settings);
254
255     return $image;
256   }
257
258   /**
259    * Builds the Blazy image as a structured array ready for ::renderer().
260    *
261    * @param array $element
262    *   The pre-rendered element.
263    *
264    * @return array
265    *   The renderable array of pre-rendered element.
266    */
267   public function preRenderImage(array $element) {
268     $build = $element['#build'];
269     $item  = $build['item'];
270     unset($element['#build']);
271
272     $settings = $build['settings'];
273     if (empty($item)) {
274       return [];
275     }
276
277     // Extract field item attributes for the theme function, and unset them
278     // from the $item so that the field template does not re-render them.
279     $item_attributes = [];
280     if (isset($item->_attributes)) {
281       $item_attributes = $item->_attributes;
282       unset($item->_attributes);
283     }
284
285     // Responsive image integration.
286     $settings['responsive_image_style_id'] = '';
287     if (!empty($settings['resimage']) && !empty($settings['responsive_image_style'])) {
288       $responsive_image_style = $this->entityLoad($settings['responsive_image_style'], 'responsive_image_style');
289       $settings['responsive_image_style_id'] = $responsive_image_style->id() ?: '';
290       $settings['lazy'] = '';
291       if (!empty($settings['responsive_image_style_id'])) {
292         if ($this->configLoad('responsive_image')) {
293           $item_attributes['data-srcset'] = TRUE;
294           $settings['lazy'] = 'responsive';
295         }
296         $element['#cache']['tags'] = $this->getResponsiveImageCacheTags($responsive_image_style);
297       }
298     }
299     else {
300       if (!isset($settings['_no_cache'])) {
301         $file_tags = isset($settings['file_tags']) ? $settings['file_tags'] : [];
302         $settings['cache_tags'] = empty($settings['cache_tags']) ? $file_tags : Cache::mergeTags($settings['cache_tags'], $file_tags);
303
304         $element['#cache']['max-age'] = -1;
305         foreach (['contexts', 'keys', 'tags'] as $key) {
306           if (!empty($settings['cache_' . $key])) {
307             $element['#cache'][$key] = $settings['cache_' . $key];
308           }
309         }
310       }
311     }
312
313     $element['#item']            = $item;
314     $element['#captions']        = empty($build['captions']) ? [] : ['inline' => $build['captions']];
315     $element['#item_attributes'] = $item_attributes;
316     $element['#url']             = '';
317     $element['#settings']        = $settings;
318
319     foreach (['caption', 'media', 'wrapper'] as $key) {
320       if (!empty($settings["$key" . '_attributes'])) {
321         $element["#$key" . '_attributes'] = $settings["$key" . '_attributes'];
322       }
323     }
324
325     if (!empty($settings['media_switch']) && $settings['media_switch'] != 'media') {
326       if ($settings['media_switch'] == 'content' && !empty($settings['content_url'])) {
327         $element['#url'] = $settings['content_url'];
328       }
329       elseif (!empty($settings['lightbox'])) {
330         BlazyLightbox::build($element);
331       }
332     }
333
334     return $element;
335   }
336
337   /**
338    * Returns the entity view, if available.
339    *
340    * @param object $entity
341    *   The entity being rendered.
342    *
343    * @return array|bool
344    *   The renderable array of the view builder, or false if not applicable.
345    */
346   public function getEntityView($entity = NULL, $settings = [], $fallback = '') {
347     if ($entity instanceof EntityInterface) {
348       $entity_type_id = $entity->getEntityTypeId();
349       $view_hook      = $entity_type_id . '_view';
350       $view_mode      = empty($settings['view_mode']) ? 'default' : $settings['view_mode'];
351       $langcode       = $entity->language()->getId();
352
353       // If module implements own {entity_type}_view.
354       if (function_exists($view_hook)) {
355         return $view_hook($entity, $view_mode, $langcode);
356       }
357       // If entity has view_builder handler.
358       elseif ($this->getEntityTypeManager()->hasHandler($entity_type_id, 'view_builder')) {
359         return $this->getEntityTypeManager()->getViewBuilder($entity_type_id)->view($entity, $view_mode, $langcode);
360       }
361       elseif ($fallback) {
362         return ['#markup' => $fallback];
363       }
364     }
365
366     return FALSE;
367   }
368
369   /**
370    * Returns the Responsive image cache tags.
371    *
372    * @param object $responsive
373    *   The responsive image style entity.
374    *
375    * @return array
376    *   The responsive image cache tags, or empty array.
377    */
378   public function getResponsiveImageCacheTags($responsive = NULL) {
379     $cache_tags = [];
380     $image_styles_to_load = [];
381     if ($responsive) {
382       $cache_tags = Cache::mergeTags($cache_tags, $responsive->getCacheTags());
383       $image_styles_to_load = $responsive->getImageStyleIds();
384     }
385
386     $image_styles = $this->entityLoadMultiple('image_style', $image_styles_to_load);
387     foreach ($image_styles as $image_style) {
388       $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags());
389     }
390     return $cache_tags;
391   }
392
393 }