725163b0193750762bc764105f9caf8eb380cc92
[yaffs-website] / web / modules / contrib / blazy / src / Blazy.php
1 <?php
2
3 namespace Drupal\blazy;
4
5 use Drupal\Core\Template\Attribute;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Utility\Unicode;
8 use Drupal\Component\Serialization\Json;
9 use Drupal\image\Entity\ImageStyle;
10 use Drupal\blazy\Dejavu\BlazyDefault;
11
12 /**
13  * Implements BlazyInterface.
14  */
15 class Blazy implements BlazyInterface {
16
17   /**
18    * Defines constant placeholder Data URI image.
19    */
20   const PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
21
22   /**
23    * The blazy HTML ID.
24    *
25    * @var int
26    */
27   private static $blazyId;
28
29   /**
30    * Prepares variables for blazy.html.twig templates.
31    */
32   public static function buildAttributes(&$variables) {
33     $element = $variables['element'];
34     foreach (['captions', 'item_attributes', 'settings', 'url'] as $key) {
35       $variables[$key] = isset($element["#$key"]) ? $element["#$key"] : [];
36     }
37
38     // Load the supported formatter variables for the possesive blazy wrapper.
39     $item             = isset($element['#item']) ? $element['#item'] : NULL;
40     $settings         = &$variables['settings'];
41     $attributes       = &$variables['attributes'];
42     $image_attributes = &$variables['item_attributes'];
43
44     // Provides sensible defaults to shut up notices when lacking of settings.
45     foreach (['icon', 'image_style', 'media_switch', 'player', 'scheme'] as $key) {
46       $settings[$key] = isset($settings[$key]) ? $settings[$key] : '';
47     }
48
49     $settings['type']      = empty($settings['type']) ? 'image' : $settings['type'];
50     $settings['ratio']     = empty($settings['ratio']) ? '' : str_replace(':', '', $settings['ratio']);
51     $settings['item_id']   = empty($settings['item_id']) ? 'blazy' : $settings['item_id'];
52     $settings['namespace'] = empty($settings['namespace']) ? 'blazy' : $settings['namespace'];
53
54     self::buildUrl($settings, $item);
55
56     // Do not proceed if no URI is provided.
57     // URI is stored within settings, not theme_blazy() property, as it is
58     // always called for different purposes prior to arriving at theme_blazy().
59     if (empty($settings['uri'])) {
60       return;
61     }
62
63     // Supports non-blazy formatter, that is, responsive image theme.
64     $image = &$variables['image'];
65     $media = !empty($settings['embed_url']) && in_array($settings['type'], ['audio', 'video']);
66
67     // Thumbnails.
68     // With CSS background, IMG may be empty, add thumbnail to the container.
69     if (!empty($settings['thumbnail_style'])) {
70       $attributes['data-thumb'] = ImageStyle::load($settings['thumbnail_style'])->buildUrl($settings['uri']);
71     }
72
73     // Check whether we have responsive image, or Blazy one.
74     if (!empty($settings['responsive_image_style_id'])) {
75       $image['#type'] = 'responsive_image';
76       $image['#responsive_image_style_id'] = $settings['responsive_image_style_id'];
77       $image['#uri'] = $settings['uri'];
78
79       // Disable aspect ratio which is not yet supported due to complexity.
80       $settings['ratio'] = FALSE;
81     }
82     else {
83       // Supports non-lazyloaded image.
84       $image['#theme'] = 'image';
85
86       // Supports either lazy loaded image, or not, which is overriden later.
87       // This allows Blazy to be used for RSS by disabling $settings['lazy']
88       // and $settings['view_mode'] = 'rss' via hook_blazy_settings_alter()
89       // since image_url is not transformed relative.
90       $image['#uri'] = empty($settings['image_url']) ? $settings['uri'] : $settings['image_url'];
91
92       // Aspect ratio to fix layout reflow with lazyloaded images responsively.
93       // This is outside 'lazy' to allow non-lazyloaded iframes use this too.
94       if (!empty($settings['width'])) {
95         if (!empty($settings['ratio']) && in_array($settings['ratio'], ['enforced', 'fluid'])) {
96           $padding_bottom = empty($settings['padding_bottom']) ? round((($settings['height'] / $settings['width']) * 100), 2) : $settings['padding_bottom'];
97           $attributes['style'] = 'padding-bottom: ' . $padding_bottom . '%';
98           $settings['_breakpoint_ratio'] = $settings['ratio'];
99         }
100
101         // Only output dimensions for non-responsive images.
102         // Respects hand-coded image attributes.
103         if (!isset($image_attributes['width'])) {
104           $image_attributes['height'] = $settings['height'];
105           $image_attributes['width']  = $settings['width'];
106         }
107       }
108
109       // Supports lazyloaded image.
110       if (!empty($settings['lazy'])) {
111         $image['#uri'] = static::PLACEHOLDER;
112
113         // Attach data attributes to either IMG tag, or DIV container.
114         if (empty($settings['background']) || empty($settings['blazy'])) {
115           self::buildBreakpointAttributes($image_attributes, $settings);
116         }
117
118         // Supports both Slick and Blazy CSS background lazyloading.
119         if (!empty($settings['background'])) {
120           self::buildBreakpointAttributes($attributes, $settings);
121           $attributes['class'][] = 'media--background';
122
123           // Blazy doesn't need IMG to lazyload CSS background. Slick does.
124           if (!empty($settings['blazy'])) {
125             $image = [];
126           }
127         }
128
129         // Multi-breakpoint aspect ratio only applies if lazyloaded.
130         if (!empty($settings['blazy_data']['dimensions'])) {
131           $attributes['data-dimensions'] = Json::encode($settings['blazy_data']['dimensions']);
132         }
133       }
134     }
135
136     // Image is optional for Video, and Blazy CSS background images.
137     if ($image) {
138       // Respects hand-coded image attributes.
139       if (!isset($image_attributes['alt'])) {
140         $image_attributes['alt'] = isset($item->alt) ? $item->alt : NULL;
141       }
142
143       // Do not output an empty 'title' attribute.
144       if (isset($item->title) && (Unicode::strlen($item->title) != 0)) {
145         $image_attributes['title'] = $item->title;
146       }
147
148       $image_attributes['class'][] = 'media__image media__element';
149       $image['#attributes'] = $image_attributes;
150     }
151
152     // Prepares a media player, and allows a tiny video preview without iframe.
153     if ($media && empty($settings['_noiframe'])) {
154       self::buildIframeAttributes($variables);
155     }
156
157     // Provides optional attributes.
158     foreach (['caption', 'media', 'url', 'wrapper'] as $key) {
159       $attr = $key . '_attributes';
160       $variables[$attr] = empty($element['#' . $attr]) ? [] : new Attribute($element['#' . $attr]);
161     }
162   }
163
164   /**
165    * Modifies variables for iframes.
166    */
167   public static function buildIframeAttributes(&$variables) {
168     // Prepares a media player, and allows a tiny video preview without iframe.
169     // image : If iframe switch disabled, fallback to iframe, remove image.
170     // player: If no colorbox/photobox, it is an image to iframe switcher.
171     // data- : Gets consistent with colorbox to share JS manipulation.
172     $settings           = &$variables['settings'];
173     $variables['image'] = empty($settings['media_switch']) ? [] : $variables['image'];
174     $settings['player'] = empty($settings['lightbox']) && $settings['media_switch'] != 'content';
175     $iframe['data-src'] = $settings['embed_url'];
176     $iframe['src']      = empty($settings['iframe_lazy']) ? $settings['embed_url'] : 'about:blank';
177
178     // Only lazyload if media switcher is empty, but iframe lazy enabled.
179     if (!empty($settings['iframe_lazy']) && empty($settings['media_switch'])) {
180       $iframe['class'][] = 'b-lazy';
181     }
182
183     // Prevents broken iframe when aspect ratio is empty.
184     if (empty($settings['ratio']) && !empty($settings['width'])) {
185       $iframe['width']  = $settings['width'];
186       $iframe['height'] = $settings['height'];
187     }
188
189     // Pass iframe attributes to template.
190     $settings['autoplay_url'] = empty($settings['autoplay_url']) ? $settings['embed_url'] : $settings['autoplay_url'];
191     $variables['iframe_attributes'] = new Attribute($iframe);
192
193     // Iframe is removed on lazyloaded, puts data at non-removable storage.
194     $variables['attributes']['data-media'] = Json::encode(['type' => $settings['type'], 'scheme' => $settings['scheme']]);
195   }
196
197   /**
198    * Provides re-usable breakpoint data-attributes.
199    *
200    * $settings['breakpoints'] must contain: xs, sm, md, lg breakpoints with
201    * the expected keys: width, image_style.
202    *
203    * @see self::buildAttributes()
204    */
205   public static function buildBreakpointAttributes(array &$attributes = [], array &$settings = []) {
206     $lazy_attribute = empty($settings['lazy_attribute']) ? 'src' : $settings['lazy_attribute'];
207
208     // Defines attributes, builtin, or supported lazyload such as Slick.
209     $attributes['class'][] = empty($settings['lazy_class']) ? 'b-lazy' : $settings['lazy_class'];
210     $attributes['data-' . $lazy_attribute] = $settings['image_url'];
211
212     // Only provide multi-serving image URLs if breakpoints are provided.
213     if (empty($settings['breakpoints'])) {
214       return;
215     }
216
217     $srcset = $json = [];
218     foreach ($settings['breakpoints'] as $key => $breakpoint) {
219       if (empty($breakpoint['image_style']) || empty($breakpoint['width'])) {
220         continue;
221       }
222
223       if ($style = ImageStyle::load($breakpoint['image_style'])) {
224         $url = $style->buildUrl($settings['uri']);
225
226         // Supports multi-breakpoint aspect ratio with irregular sizes.
227         // Yet, only provide individual dimensions if not already set.
228         // @see Drupal\blazy\BlazyManager::setDimensionsOnce().
229         if (!empty($settings['_breakpoint_ratio']) && empty($settings['blazy_data']['dimensions'])) {
230           $dimensions = [
231             'width'  => $settings['width'],
232             'height' => $settings['height'],
233           ];
234
235           $style->transformDimensions($dimensions, $settings['uri']);
236           if ($width = self::widthFromDescriptors($breakpoint['width'])) {
237             $json[$width] = round((($dimensions['height'] / $dimensions['width']) * 100), 2);
238           }
239         }
240
241         $settings['breakpoints'][$key]['url'] = $url;
242
243         // @todo: Recheck library if multi-styled BG is still supported anyway.
244         // Confirmed: still working with GridStack multi-image-style per item.
245         if (!empty($settings['background'])) {
246           $attributes['data-src-' . $key] = $url;
247         }
248         elseif (!empty($breakpoint['width'])) {
249           $width = trim($breakpoint['width']);
250           $width = is_numeric($width) ? $width . 'w' : $width;
251           $srcset[] = $url . ' ' . $width;
252         }
253       }
254     }
255
256     if ($srcset) {
257       $settings['srcset'] = implode(', ', $srcset);
258
259       $attributes['srcset'] = '';
260       $attributes['data-srcset'] = $settings['srcset'];
261       $attributes['sizes'] = '100w';
262
263       if (!empty($settings['sizes'])) {
264         $attributes['sizes'] = trim($settings['sizes']);
265         unset($attributes['height'], $attributes['width']);
266       }
267     }
268
269     if ($json) {
270       $settings['blazy_data']['dimensions'] = $json;
271     }
272   }
273
274   /**
275    * Builds URLs, cache tags, and dimensions for individual image.
276    */
277   public static function buildUrl(array &$settings = [], $item = NULL) {
278     // Blazy already sets URI, yet set fallback for direct theme_blazy() call.
279     if (empty($settings['uri']) && $item) {
280       $settings['uri'] = ($entity = $item->entity) && empty($item->uri) ? $entity->getFileUri() : $item->uri;
281     }
282
283     if (empty($settings['uri'])) {
284       return;
285     }
286
287     // Lazyloaded elements expect image URL, not URI.
288     if (empty($settings['image_url'])) {
289       $settings['image_url'] = file_create_url($settings['uri']);
290     }
291
292     // Sets dimensions.
293     // VEF without image style, or image style with crop, may already set these.
294     if (empty($settings['width'])) {
295       $settings['width']  = isset($item->width) ? $item->width : NULL;
296       $settings['height'] = isset($item->height) ? $item->height : NULL;
297     }
298
299     // Image style modifier can be multi-style images such as GridStack.
300     if (!empty($settings['image_style']) && ($style = ImageStyle::load($settings['image_style']))) {
301       // Image URLs, as opposed to URIs, are expected by lazyloaded images.
302       $settings['image_url']  = $style->buildUrl($settings['uri']);
303       $settings['cache_tags'] = $style->getCacheTags();
304
305       // Only re-calculate dimensions if not cropped, nor already set.
306       if (empty($settings['_dimensions'])) {
307         $dimensions = [
308           'width'  => $settings['width'],
309           'height' => $settings['height'],
310         ];
311
312         $style->transformDimensions($dimensions, $settings['uri']);
313         $settings['height'] = $dimensions['height'];
314         $settings['width']  = $dimensions['width'];
315       }
316     }
317   }
318
319   /**
320    * Gets the numeric "width" part from a descriptor.
321    */
322   public static function widthFromDescriptors($descriptor = '') {
323     // Dynamic multi-serving aspect ratio with backward compatibility.
324     $descriptor = trim($descriptor);
325     if (is_numeric($descriptor)) {
326       return (int) $descriptor;
327     }
328
329     // Cleanup w descriptor to fetch numerical width for JS aspect ratio.
330     $width = strpos($descriptor, "w") !== FALSE ? str_replace('w', '', $descriptor) : $descriptor;
331
332     // If both w and x descriptors are provided.
333     if (strpos($descriptor, " ") !== FALSE) {
334       // If the position is expected: 640w 2x.
335       list($width, $px) = array_pad(array_map('trim', explode(" ", $width, 2)), 2, NULL);
336
337       // If the position is reversed: 2x 640w.
338       if (is_numeric($px) && strpos($width, "x") !== FALSE) {
339         $width = $px;
340       }
341     }
342
343     return is_numeric($width) ? (int) $width : FALSE;
344   }
345
346   /**
347    * Overrides variables for responsive-image.html.twig templates.
348    */
349   public static function preprocessResponsiveImage(&$variables) {
350     $config = self::getConfig();
351
352     // Prepare all <picture> [data-srcset] attributes on <source> elements.
353     if (!$variables['output_image_tag']) {
354       /** @var \Drupal\Core\Template\Attribute $source */
355       foreach ($variables['sources'] as &$source) {
356         $srcset = $source['srcset'];
357         $srcset_values = $srcset->value();
358
359         $source->setAttribute('data-srcset', $srcset_values);
360         $source->removeAttribute('srcset');
361       }
362
363       // Fetches the picture element fallback URI, and empty it later.
364       // These address both 8.x-2 and 8.x-3 compatibility.
365       if (isset($variables['img_element']['#srcset'])) {
366         $fallback_uri = $variables['img_element']['#srcset'][0]['uri'];
367       }
368       else {
369         $fallback_uri = $variables['img_element']['#uri'];
370       }
371
372       // Cleans up the no-longer relevant attributes for controlling element.
373       unset($variables['attributes']['data-srcset'], $variables['img_element']['#attributes']['data-srcset']);
374       $variables['img_element']['#srcset'] = '';
375
376       // Prevents invalid IMG tag when one pixel placeholder is disabled.
377       $variables['img_element']['#uri'] = static::PLACEHOLDER;
378     }
379     else {
380       $srcset = $variables['attributes']['srcset'];
381       $srcset_values = $srcset->value();
382       $fallback_uri = $variables['img_element']['#uri'];
383
384       $variables['attributes']['data-srcset'] = $srcset_values;
385       $variables['img_element']['#attributes']['data-srcset'] = $srcset_values;
386       $variables['img_element']['#attributes']['srcset'] = '';
387     }
388
389     // Blazy needs controlling element to have fallback [data-src], else error.
390     $variables['img_element']['#attributes']['data-src'] = $fallback_uri;
391     $variables['img_element']['#attributes']['class'][] = 'b-lazy b-responsive';
392
393     // Only replace fallback image URI with 1px placeholder, if so configured.
394     // This prevents double-downloading the fallback image.
395     if ($config['one_pixel']) {
396       $variables['img_element']['#uri'] = static::PLACEHOLDER;
397     }
398
399     $variables['img_element']['#attached']['drupalSettings']['blazy'] = $config['blazy'];
400   }
401
402   /**
403    * Implements hook_config_schema_info_alter().
404    */
405   public static function configSchemaInfoAlter(array &$definitions, $formatter = 'blazy_base', $settings = []) {
406     if (isset($definitions[$formatter])) {
407       $mappings = &$definitions[$formatter]['mapping'];
408       $settings = $settings ?: BlazyDefault::extendedSettings() + BlazyDefault::gridSettings();
409       foreach ($settings as $key => $value) {
410         // Seems double is ignored, and causes a missing schema, unlike float.
411         $type = gettype($value);
412         $type = $type == 'double' ? 'float' : $type;
413         $mappings[$key]['type'] = $key == 'breakpoints' ? 'mapping' : (is_array($value) ? 'sequence' : $type);
414
415         if (!is_array($value)) {
416           $mappings[$key]['label'] = Unicode::ucfirst(str_replace('_', ' ', $key));
417         }
418       }
419
420       if (isset($mappings['breakpoints'])) {
421         foreach (BlazyDefault::getConstantBreakpoints() as $breakpoint) {
422           $mappings['breakpoints']['mapping'][$breakpoint]['type'] = 'mapping';
423           foreach (['breakpoint', 'width', 'image_style'] as $item) {
424             $mappings['breakpoints']['mapping'][$breakpoint]['mapping'][$item]['type']  = 'string';
425             $mappings['breakpoints']['mapping'][$breakpoint]['mapping'][$item]['label'] = Unicode::ucfirst(str_replace('_', ' ', $item));
426           }
427         }
428       }
429
430       // @todo: Drop non-UI stuffs.
431       foreach (['dimension', 'display', 'item_id'] as $key) {
432         $mappings[$key]['type'] = 'string';
433       }
434     }
435   }
436
437   /**
438    * Return blazy global config.
439    */
440   public static function getConfig($setting_name = '', $settings = 'blazy.settings') {
441     $config = \Drupal::service('config.factory')->get($settings);
442     return empty($setting_name) ? $config->get() : $config->get($setting_name);
443   }
444
445   /**
446    * Returns the trusted HTML ID of a single instance.
447    */
448   public static function getHtmlId($string = 'blazy', $id = '') {
449     if (!isset(static::$blazyId)) {
450       static::$blazyId = 0;
451     }
452
453     // Do not use dynamic Html::getUniqueId, otherwise broken AJAX.
454     return empty($id) ? Html::getId($string . '-' . ++static::$blazyId) : strip_tags($id);
455   }
456
457   /**
458    * Checks if an image style contains crop effect.
459    *
460    * @deprecated: Removed for BlazyManager to avoid static method dependency.
461    */
462   public static function isCrop($style = NULL) {
463     return \Drupal::service('blazy.manager')->isCrop($style);
464   }
465
466 }