/o//commerce-media/accounts/-1/images/18648322?download=true
The following has evaluated to null or missing: ==> filteredProductImages[0] [in template "3192443#3192485#null" at line 27, column 31] ---- Tip: It's the final [] step that caused this error, not those before it. ---- Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)?? ---- ---- FTL stack trace ("~" means nesting-related): - Failed at: ${filteredProductImages[0].title?html} [in template "3192443#3192485#null" at line 27, column 29] ----
1<#assign
2 channel = restClient.get("/headless-commerce-delivery-catalog/v1.0/channels?accountId=-1&filter=siteGroupId eq '${themeDisplay.getScopeGroupId()}'")
3
4 productImagesResponse = restClient.get(
5 "/headless-commerce-delivery-catalog/v1.0/channels/" + channel.items[0].id +
6 "/products/" + CPDefinition_cProductId.getData() + "/images?accountId=-1"
7 )
8
9 allProductImages = productImagesResponse.items![]
10 filteredProductImages = []
11>
12
13<#list allProductImages as image>
14 <#if image.galleryEnabled?? && image.galleryEnabled>
15 <#assign filteredProductImages += [image] />
16 </#if>
17</#list>
18
19<#assign totalCount = filteredProductImages?size >
20
21<div class = "carousel-container">
22 <div class = "main-image-wrapper">
23 <button class = "nav-button prev" aria-label = "Previous Image">
24 <span class = "lexicon-icon-overwide"> <@clay["icon"] symbol = "angle-left" /></span>
25 </button>
26
27 <img alt = "${filteredProductImages[0].title?html}" id = "main-image" src = "${(filteredProductImages[0].src?replace("https://", "http://"))}" />
28
29 <button class="nav-button next" aria-label="Next Image">
30 <span class="lexicon-icon-overwide"> <@clay["icon"] symbol="angle-right" /></span>
31 </button>
32 </div>
33
34 <div class="thumbnails-wrapper">
35 <div class="thumbnails align-items-center"></div>
36
37 <#assign count = (totalCount?default(0)?number) />
38
39 <#if count gt 5>
40 <button class="view-full-gallery">
41 <span class="title">
42 ${languageUtil.get(locale, "full-gallery", "Full Gallery")}
43 </span>
44 <span class="subtitle">
45 ${count} ${languageUtil.get(locale, "photos", "Photos")}
46 </span>
47 </button>
48 </#if>
49 </div>
50 </div>
51
52<template id="modal-gallery">
53 <div class="modal-gallery-content">
54 <button class="modal-prev" data-role="modal-prev">
55 <@clay["icon"] symbol="angle-left" />
56 </button>
57
58 <img class="modal-image" data-role="modal-image" />
59
60 <button class="modal-next" data-role="modal-next">
61 <@clay["icon"] symbol="angle-right" />
62 </button>
63 </div>
64</template>
65
66<script ${nonceAttribute}>
67(function () {
68 let currentIndex = 0;
69 let images = [];
70
71 const carouselNextBtn = document.querySelector('.nav-button.next');
72 const carouselPrevBtn = document.querySelector('.nav-button.prev');
73 const carouselMainImage = document.getElementById('main-image');
74 const thumbnailsContainer = document.querySelector('.thumbnails');
75 const viewFullGalleryBtn = document.querySelector('.view-full-gallery');
76
77 function loadImages() {
78 images = [
79 <#list filteredProductImages as image>
80 {
81 src: "${(image.src?replace('https://', 'http://'))?js_string}",
82 alt: "${image.title?html?js_string}"
83 }<#if image_has_next>,</#if>
84 </#list>
85 ]
86 }
87
88 function renderThumbnails() {
89 const maxVisible = 5;
90 let start = currentIndex - 2;
91
92 if (start < 0) start = 0;
93 if (start > images.length - maxVisible) start = Math.max(images.length - maxVisible, 0);
94
95 const end = Math.min(images.length, start + maxVisible);
96
97 thumbnailsContainer.innerHTML = '';
98
99 for (let i = start; i < end; i++) {
100 const img = document.createElement('img');
101 img.className = 'thumbnail' + (i === currentIndex ? ' selected' : '');
102 img.src = images[i].src;
103 img.alt = images[i].alt;
104 img.dataset.index = i;
105 img.addEventListener('click', () => updateMainImage(i));
106 thumbnailsContainer.appendChild(img);
107 }
108 }
109
110 function updateMainImage(index) {
111 currentIndex = index;
112 carouselMainImage.src = images[index].src;
113 carouselMainImage.alt = images[index].alt;
114
115 carouselPrevBtn.disabled = index === 0;
116 carouselNextBtn.disabled = index === images.length - 1;
117
118 renderThumbnails();
119 }
120
121 function setupNavigationButtons() {
122 carouselPrevBtn.addEventListener('click', () => {
123 if (currentIndex > 0) updateMainImage(currentIndex - 1);
124 });
125
126 carouselNextBtn.addEventListener('click', () => {
127 if (currentIndex < images.length - 1) updateMainImage(currentIndex + 1);
128 });
129 }
130
131 function setupModalTriggers() {
132 carouselMainImage.addEventListener('click', () => openModalGallery(currentIndex));
133 if (viewFullGalleryBtn) {
134 viewFullGalleryBtn.addEventListener('click', () => openModalGallery(currentIndex));
135 }
136 }
137
138 function openModalGallery(startIndex) {
139 let current = startIndex;
140
141 const template = document.getElementById('modal-gallery');
142 const clone = template.content.cloneNode(true);
143 const container = document.createElement('div');
144 container.appendChild(clone);
145
146 Liferay.Util.openModal({
147 bodyHTML: container.innerHTML,
148 center: true,
149 headerHTML: '<h2 class="modal-gallery-header" id="modal-header-title"><@clay["icon"] symbol="picture"/> ${languageUtil.get(locale, "Image")} <span id="modal-index-display"></span></h2>',
150 size: "full-screen",
151 onOpen: () => {
152 const modalContainer = document.querySelector('.modal-content');
153 if (modalContainer) {
154 modalContainer.classList.add('custom-gallery-modal');
155 }
156
157 const modalImage = document.querySelector('[data-role="modal-image"]');
158 const modalNext = document.querySelector('[data-role="modal-next"]');
159 const modalPrev = document.querySelector('[data-role="modal-prev"]');
160 const indexDisplay = document.getElementById('modal-index-display');
161
162 function updateModalImage(index) {
163 const img = images[index];
164 modalImage.src = img.src;
165 modalImage.alt = img.alt;
166
167 modalNext.disabled = index === images.length - 1;
168 modalPrev.disabled = index === 0;
169
170 if (indexDisplay) {
171 indexDisplay.textContent = (index + 1) + ' ${languageUtil.get(locale, "of")} ' + images.length;
172 }
173 }
174
175 modalNext.addEventListener('click', () => {
176 if (current < images.length - 1) {
177 current++;
178 updateModalImage(current);
179 }
180 });
181
182 modalPrev.addEventListener('click', () => {
183 if (current > 0) {
184 current--;
185 updateModalImage(current);
186 }
187 });
188
189 updateModalImage(current);
190 }
191 });
192 }
193
194 function main() {
195 loadImages();
196 setupNavigationButtons();
197 setupModalTriggers();
198 updateMainImage(0);
199 }
200
201 main();
202})();
203</script>
204
205<style ${nonceAttribute}>
206.carousel-container img {
207 cursor: pointer;
208 object-fit: contain;
209}
210
211.custom-gallery-modal button:disabled {
212 cursor: default;
213 opacity: 0.4;
214 pointer-events: none;
215}
216
217.custom-gallery-modal {
218 background-color: #282934 !important;
219 border-bottom: none;
220 color: white !important;
221}
222
223.custom-gallery-modal .liferay-modal-body {
224 align-items: center;
225 display: flex;
226 justify-content: center;
227 position: relative;
228}
229
230.custom-gallery-modal .close {
231 color: white !important;
232 margin-right: 16px !important;
233}
234
235.lexicon-icon-overwide .lexicon-icon {
236 height: 2em;
237 margin: 0px !important;
238}
239
240.custom-gallery-modal{
241 height: 80% !important;
242}
243
244.main-image-wrapper {
245 align-items: center;
246 display:flex;
247 justify-content: center;
248 position: relative;
249 width: 902px;
250 height: 454px;
251}
252
253.main-image-wrapper img {
254 border-radius: 8px;
255 max-height: 100%;
256}
257
258.main-image-wrapper:hover .nav-button {
259 opacity: 1;
260 pointer-events: auto;
261}
262
263.main-image-wrapper:hover .nav-button:disabled{
264 cursor: default;
265 opacity: 0.4;
266}
267
268.modal-image {
269 aspect-ratio: 16/9;
270 object-fit: contain;;
271 border-radius: 8px;
272 max-width: 100vh !important;
273}
274
275.modal-gallery-header .lexicon-icon {
276 fill: #FFC124 !important;
277 margin-right: 8px !important;
278 width: 16px !important;
279}
280
281.modal-gallery-header {
282
283 padding: 16px !important;
284}
285
286.modal-prev,
287.modal-next {
288 align-items: center;
289 background: rgba(105, 102, 102, 0.4) !important;
290 border-radius: 50%;
291 border: none;
292 display: flex;
293 font-size: 1.6rem;
294 justify-content: center;
295 padding: 14px !important;
296 position: absolute;
297 top: 45%;
298}
299
300.modal-prev .lexicon-icon,
301.modal-next .lexicon-icon {
302 margin-top: 0px !important;
303}
304
305.modal-next {
306 right: 24px;
307}
308
309.modal-prev {
310 left: 24px;
311}
312
313.nav-button {
314 background: rgba(0,0,0,0.4);
315 border-radius: 50%;
316 border: none;
317 color: white;
318 cursor: pointer;
319 font-size: 1rem;
320 opacity: 0;
321 padding: 0 8px;
322 position: absolute;
323 top: 50%;
324 transform: translateY(-50%);
325 transition: opacity 0.3s ease;
326 user-select: none;
327}
328
329.nav-button.prev {
330 left: 10px;
331}
332
333.nav-button.next {
334 right: 10px;
335}
336
337.thumbnail {
338 border-radius: 12px;
339 border: 2px solid transparent;
340 cursor: pointer;
341 height: 86px;
342 object-fit: cover;
343 opacity: 0.6;
344 transition:
345 border-color 250ms ease-out,
346 opacity 250ms ease-out;
347 width: 142px;
348}
349
350.thumbnail.selected,
351.thumbnail:hover {
352 border-color: #8FB5FF;
353 opacity: 1;
354}
355
356.thumbnail.selected{
357 height: 102px;
358}
359
360.thumbnails {
361 display: flex;
362 gap: 8px;
363 overflow-x: auto;
364}
365
366.thumbnails-wrapper {
367 align-items: center;
368 display: flex;
369 justify-content: flex-start;
370 margin-top: 24px;
371 max-height: 86px;
372 max-width: 902px;
373}
374
375.view-full-gallery {
376 background-color: white;
377 border-radius: 12px;
378 border: 1px solid #E2E2E4;
379 color: #2563eb;
380 cursor: pointer;
381 display: flex;
382 flex-direction: column;
383 height: 86px;
384 justify-content: center;
385 margin-left: 8px;
386 min-width: 152px;
387 transition: background-color 0.3s ease,
388 box-shadow 0.3s ease,
389 border-color 250ms ease-out;
390}
391
392.view-full-gallery .subtitle {
393 color: #6b7280;
394 font-size: 12px;
395 font-weight: 400;
396 line-height: 1;
397}
398
399.view-full-gallery .title {
400 font-size: 16px;
401 font-weight: 600;
402 line-height: 1;
403 margin-bottom: 4px;
404}
405
406.view-full-gallery:hover {
407 background-color: #f3f4f6;
408 box-shadow: 0 2px 4px rgb(0 0 0 / 0.1);
409 border: 2px solid rgb(143, 181, 255);
410}
411</style>
With this new custom permission, frontend developers can leverage the Liferay permission system to show/hide the portal dockbar inside a custom theme.
Here's how to initialize a Freemarker variable inside the init_custom.ftl file of a custom theme:
<#assign viewDockbar = permissionChecker.hasPermission(themeDisplay.scopeGroupId, "90", "90", "VIEW_DOCKBAR") />
And here's how to use the Freemarker variable:
<#if viewDockbar>
<@liferay.control_menu />
</#if>
On Liferay 7.4+ you need to add this property to your portal-ext.properties file in order to make it working:
resource.actions.strict.mode.enabled=false
With this new custom permission, frontend developers can leverage the Liferay permission system to show/hide the portal dockbar inside a custom theme.
Here's how to initialize a Freemarker variable inside the init_custom.ftl file of a custom theme:
<#assign viewDockbar = permissionChecker.hasPermission(themeDisplay.scopeGroupId, "90", "90", "VIEW_DOCKBAR") />
And here's how to use the Freemarker variable:
<#if viewDockbar>
<@liferay.control_menu />
</#if>
On Liferay 7.4+ you need to add this property to your portal-ext.properties file in order to make it working:
resource.actions.strict.mode.enabled=false
Developer
Publisher Date
January 24, 2024
Deployment Method
Liferay Self-Hosted
Liferay PaaS
App Type
Version
Supported Versions
Standard Price
Help and Support
Share Link
HTML Example
A paragraph is a self-contained unit of a discourse in writing dealing with a particular point or idea. Paragraphs are usually an expected part of formal writing, used to organize longer prose.