PageSourceSearch

https://www.gkb.ch/shared-blocks/frontify/index.js

js gkb.ch collected 2026-10-02 07:33:04 UTC 9,857 bytes, 242 lines download raw bytes

1// TODO: consider making this a bit more flexible with breakpoints and sizes
2// For now all randomly hardcoded based on "common" breakpoints/sizes
3
4// TODO: convert into a more flexible mapping object sizes/breakpoint structure. Idea:
5// {
6//   sm: { breakpoint: 671, sizes: [320, 480, 640] },
7//   md: { breakpoint: 1055, sizes: [768, 1024] },
8//   lg: { breakpoint: 1599, sizes: [1280, 1440] },
9//   max: { breakpoint: 1600, sizes: [1600, 1920] },
10// }
11// Should always be aligned to scripts/breakpoints.js
12const defaultMediaQueryBreakpoints = [671, 1055, 1599, 1600];
13const defaultMediaQueryBreakpointsMapping = {
14  671: 'sm',
15  1055: 'md',
16  1599: 'lg',
17  1600: 'max',
18};
19const defaultSrcSizes = [320, 480, 640, 768, 1024, 1280, 1440, 1600, 1920];
20const defaultResize = 320;
21
22/**
23 * Generates the sizes attribute for responsive images based on columns map per breakpoint
24 * @param {Object} columnsMap - Number of columns per breakpoint (e.g., { sm: 1, md: 2, lg: 2, max: 2 })
25 * @returns {string} The sizes attribute value
26 */
27export function generateSizesFromColumnsMap(columnsMap) {
28  // Grid system constants - matches styles/base/grid.css and styles/ui.css
29  const MAX_CONTENT_WIDTH = 1200; // --page-max-content-width in styles/ui.css
30  const SIDE_MARGIN = 24; // --side-margin in styles/base/grid.css
31  const MARGIN_TOTAL = SIDE_MARGIN * 2; // Total horizontal margins
32
33  // Extract breakpoint pixel values from mapping
34  // Converts { 671: 'sm', 1055: 'md', ... } to { sm: 671, md: 1055, ... }
35  const breakpointValues = {};
36  Object.entries(defaultMediaQueryBreakpointsMapping).forEach(([px, name]) => {
37    breakpointValues[name] = parseInt(px, 10);
38  });
39  // Result: { sm: 671, md: 1055, lg: 1599, max: 1600 }
40
41  // Resolve column count for a breakpoint with fallback chain
42  // Fallback order: max → lg → md → sm → 1
43  const getColumns = (breakpoint) => {
44    // If explicitly set, use it
45    if (columnsMap[breakpoint]) {
46      return columnsMap[breakpoint];
47    }
48
49    // Fallback chain
50    const fallbacks = { max: 'lg', lg: 'md', md: 'sm', sm: null };
51    const fallbackBreakpoint = fallbacks[breakpoint];
52
53    // Recursively check fallback, or default to 1 column
54    return fallbackBreakpoint ? getColumns(fallbackBreakpoint) : 1;
55  };
56
57  // Pre-resolve all column values
58  const cols = {
59    max: getColumns('max'),
60    lg: getColumns('lg'),
61    md: getColumns('md'),
62    sm: getColumns('sm'),
63  };
64
65  // Build sizes array - only add when columns change from previous breakpoint
66  const sizes = [];
67
68  if (cols.max) {
69    sizes.push(`(min-width: ${MAX_CONTENT_WIDTH}px) ${MAX_CONTENT_WIDTH / cols.max}px`);
70  }
71
72  if (cols.lg !== cols.max) {
73    sizes.push(`(min-width: ${breakpointValues.lg + 1}px) calc(${100 / cols.lg}vw - ${MARGIN_TOTAL}px)`);
74  }
75
76  // Add md breakpoint when columns differ from sm
77  // Use the actual md min breakpoint (672) not md+1 to cover the tablet range
78  if (cols.md !== cols.sm) {
79    sizes.push(`(min-width: ${breakpointValues.sm + 1}px) calc(${100 / cols.md}vw - ${MARGIN_TOTAL}px)`);
80  }
81
82  // Mobile fallback
83  sizes.push(`calc(${100 / cols.sm}vw - ${MARGIN_TOTAL}px)`);
84
85  return sizes.join(', ');
86}
87
88/**
89 * Frontify class to handle image rendering with responsive breakpoints.
90 * It constructs the image source URL based on the provided options and Frontify JSON data.
91 * It supports responsive images with srcset and picture elements.
92 *
93 * Options:
94 * - simpleImg: boolean - If true, renders a simple <img> tag instead of <picture> element
95 * - fixedWidth: number - Fixed width for simple image (defaults to defaultResize if not specified)
96 * - imageAlt: string - Alt text for the image
97 * - classes: string - CSS classes to add to the image
98 * - loading: string - Loading strategy ('lazy' or 'eager')
99 * - breakpoints: array - Custom breakpoints for responsive images
100 * - srcSizes: array - Available image sizes for srcset
101 * - defaultResize: number - Default resize value
102 * - cover: object - Cover parameters per breakpoint (e.g., { sm: '5:4', md: '5:4', lg: '5:4', max: '13:8' })
103 * - focus: string - Focus point for image cropping
104 * - columns: object - Number of columns per breakpoint (e.g., { sm: 1, md: 2, lg: 2, max: 2 })
105 * - sizes: string - Explicit sizes attribute (overrides columns-based calculation)
106 */
107export class Frontify {
108  constructor(data) {
109    this.data = data || {};
110    this.frontifyJson = this.data.frontifyJson || null;
111    this.isPDF = Boolean(this.frontifyJson?.extension?.toLowerCase() === 'pdf');
112    this.previewUrl = this.frontifyJson?.previewUrl || '';
113    this.options = this.data.options || {};
114
115    this.breakpoints = this.options.breakpoints || defaultMediaQueryBreakpoints;
116    this.srcSizes = this.options.srcSizes || defaultSrcSizes;
117    this.defaultResize = this.options.defaultResize || defaultResize;
118
119    this.imageAlt = this.options.imageAlt || this.frontifyJson?.alternativeText || '';
120    this.classes = this.options.classes || '';
121    this.loading = this.options.loading || 'lazy';
122    this.simpleImg = this.options.simpleImg || false;
123    this.fixedWidth = this.options.fixedWidth || null;
124    this.fixedHeight = this.options.fixedHeight || null;
125
126    // Compute sizes: use explicit sizes if provided, otherwise generate from columns map
127    this.computedSizes =
128      this.options.sizes || (this.options.columns ? generateSizesFromColumnsMap(this.options.columns) : '100vw');
129  }
130
131  addMediaProcessingAPIParams({ size = this.defaultResize, breakpoint = 'lg' }) {
132    const { cover, focus } = this.options;
133    const focalPoint = Array.isArray(this.data.frontifyJson.focalPoint)
134      ? `${Math.ceil(parseFloat(this.data.frontifyJson.focalPoint[0]) * 100)}px${Math.ceil(parseFloat(this.data.frontifyJson.focalPoint[1]) * 100)}p`
135      : '';
136    const mod = 'mod=v1';
137    const focusParam = `focus=${focus || focalPoint || 'auto'}`;
138    const coverParam = cover?.[breakpoint] ? `cover=${cover[breakpoint]}` : '';
139    const outputParam = 'output=webp';
140    const resizeParam = `resize=${size}`;
141    const qualityParam = 'quality=75';
142
143    return `${[mod, outputParam, focusParam, coverParam, resizeParam, qualityParam].filter(
143Boolean).join('/')}`;
144  }
145
146  /**
147   * Renders the default image element with the specified resize value.
148   * @returns {string} The HTML string for the default image element.
149   */
150  renderDefaultImage() {
151    const fetchPriority = this.loading === 'eager' ? ' fetchpriority="high"' : '';
152    return `<img src="${this.previewUrl}?${this.addMediaProcessingAPIParams({})}"
153                alt="${this.imageAlt}"
154                class="frontify-img ${this.classes}"
155                loading="${this.loading}"${fetchPriority}
156                sizes="${this.computedSizes}" />`;
157  }
158
159  /**
160   * Renders the srcset attribute for the image based on the provided condition.
161   * @param {function} condition - A function that takes a size and returns true if the size should be included in  the srcset.
162   * @param {number} breakpoint - The breakpoint value for mapping to the appropriate breakpoint name.
163   * @return {string} The srcset string for the image.
164   */
165  renderSrcSet(condition, breakpoint) {
166    let srcSet = '';
167    this.srcSizes.map((size) => {
168      if (condition(size)) {
169        srcSet += `${this.previewUrl}?${this.addMediaProcessingAPIParams({ size, breakpoint: defaultMediaQueryBreakpointsMapping[breakpoint] })} ${size}w, `;
170      }
171    });
172    return srcSet.slice(0, -2); // Remove trailing comma and space
173  }
174
175  /**
176   * Renders the source elements for the picture element based on the breakpoints.
177   * @returns {string} The HTML string for the source elements.
178   */
179  renderSources() {
180    return this.breakpoints
181      ?.map((breakpoint, index) => {
182        const sizesAttr = this.computedSizes;
183        // Include all available image sizes - let the browser pick the optimal one based on sizes attribute
184        const srcset = this.renderSrcSet(() => true, breakpoint);
185
186        if (index === 0) {
187          return `<source media="(max-width: ${breakpoint}px)" srcset="${srcset}" sizes="${sizesAttr}" />`;
188        } else if (index === this.breakpoints.length - 1) {
189          return `<source media="(min-width: ${breakpoint}px)" srcset="${srcset}" sizes="${sizesAttr}" />`;
190        }
191        const prevBreakpoint = this.breakpoints[index - 1];
192        return `<source media="(min-width: ${prevBreakpoint + 1}px) and (max-width: ${breakpoint}px)" srcset="${srcset}" sizes="${sizesAttr}" />`;
193      })
194      .join('\n');
195  }
196
197  /**
198   * Renders a simple image element with a fixed width.
199   * @returns {string} The HTML string for a simple image element.
200   */
201  renderSimpleImage() {
202    const width = this.fixedWidth || this.defaultResize;
203    const height = this.fixedHeight;
204    const fetchPriority = this.loading === 'eager' ? ' fetchpriority="high"' : '';
205    return `<img src="${this.previewUrl}?${this.addMediaProcessingAPIParams({ size: width })}"
206              alt="${this.imageAlt}"
207              class="frontify-img ${this.classes}"
208              loading="${this.loading}"${fetchPriority}
209              ${width && height ? `width="${width}" height="${height}"` : ''}/>`;
210  }
211
212  /**
213   * Renders the pdf link.
214   * @returns {string} The HTML string for pdf link.
215   */
216  renderPDFLink() {
217    return `<div class="pdf-download">
218              <a href="${this.frontifyJson.previewUrl}" download target="_blank" rel="noopener noreferrer">
219                <span>${this.frontifyJson.filename}</span>
220              </a>
221            </div>`;
222  }
223
224  render() {
225    if (!this.frontifyJson || !this.previewUrl) {
226      return null;
227    }
228
229    if (this.isPDF) {
230      return this.renderPDFLink();
231    }
232
233    if (this.simpleImg) {
234      return this.renderSimpleImage();
235    }
236
237    return `<picture>
238        ${this.renderSources()}
239        ${this.renderDefaultImage()}
240    </picture>`;
241  }
242}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.