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