From 9ae7b0631bc7faee4046203d3561ffdb4ce2dc1e Mon Sep 17 00:00:00 2001 From: kdeterme Date: Mon, 14 Sep 2026 16:37:34 +0200 Subject: [PATCH] feat: add robust panel type image normalization, text fallback rendering, and corresponding styles --- .../static/assets/sign_panels_shared.js | 166 ++++++++++++++++++ loko/assets/static/assets/unified_index.js | 151 ++++++++++++++-- loko/sign/static/sign/sign.js | 38 ++-- loko/sign/templates/sign/index.html | 81 ++++++++- loko/sign/templates/sign/sign_panel_form.html | 64 ++++++- 5 files changed, 467 insertions(+), 33 deletions(-) diff --git a/loko/assets/static/assets/sign_panels_shared.js b/loko/assets/static/assets/sign_panels_shared.js index c5254c2..d994fca 100644 --- a/loko/assets/static/assets/sign_panels_shared.js +++ b/loko/assets/static/assets/sign_panels_shared.js @@ -385,6 +385,168 @@ }; } + function normalizePanelTypeCodeForImage(code) { + let normalized = String(code || '').trim(); + if (!normalized) return ''; + + const upper = normalized.toUpperCase(); + if (upper === 'XA') { + normalized = 'GXA'; + } else if (upper === 'XB') { + normalized = 'GXB'; + } else if (upper === 'XD') { + normalized = 'GXD'; + } else if (upper.startsWith('Z') && upper.length > 1) { + normalized = normalized.slice(1); + } + + return normalized.replace(/["']/g, '').trim(); + } + + function buildPanelTypeImageCandidates(code) { + const normalizedCode = normalizePanelTypeCodeForImage(code); + if (!normalizedCode) return []; + + const upperCode = normalizedCode.toUpperCase(); + const baseSet = new Set([upperCode]); + + const strippedPrefix = upperCode.replace(/^[-_]+/, ''); + if (strippedPrefix) { + baseSet.add(strippedPrefix); + baseSet.add(strippedPrefix.replace(/-/g, '_')); + } + + if (upperCode.includes(' ')) { + baseSet.add(upperCode.replace(/\s+/g, '')); + baseSet.add(upperCode.replace(/\s+/g, '_')); + } + + if (upperCode.includes('-')) { + baseSet.add(upperCode.replace(/-/g, '_')); + } + + const uniqueCodes = []; + baseSet.forEach(item => { + if (!item) return; + const up = item.toUpperCase(); + const title = up.charAt(0) + up.slice(1).toLowerCase(); + const low = up.toLowerCase(); + [up, title, low].forEach(c => { + if (!uniqueCodes.includes(c)) uniqueCodes.push(c); + }); + }); + + const candidates = []; + uniqueCodes.forEach(item => { + candidates.push(`/static/assets/road_signs/2025/${encodeURIComponent(item)}.svg`); + }); + uniqueCodes.forEach(item => { + candidates.push(`/static/assets/road_signs/2025/${encodeURIComponent(item)}.png`); + }); + + return candidates; + } + + function renderPanelTextFallback(container, code, options = {}) { + if (!container) return; + const text = String(code || '').trim(); + if (!text) { + container.innerHTML = ''; + return; + } + + const len = text.length; + let fontSize; + let lineHeight = '1.1'; + + if (len <= 4) { + fontSize = '0.72rem'; + lineHeight = '1.15'; + } else if (len <= 8) { + fontSize = '0.62rem'; + lineHeight = '1.1'; + } else if (len <= 14) { + fontSize = '0.54rem'; + lineHeight = '1.05'; + } else if (len <= 24) { + fontSize = '0.48rem'; + lineHeight = '1.05'; + } else { + fontSize = '0.42rem'; + lineHeight = '1.0'; + } + + const isSmallThumb = options.width && parseInt(options.width, 10) < 35; + if (isSmallThumb) { + fontSize = len <= 4 ? '0.55rem' : '0.40rem'; + } + + container.innerHTML = ''; + const fallbackBox = document.createElement('div'); + fallbackBox.className = 'panel-text-fallback'; + fallbackBox.title = text; + + const span = document.createElement('span'); + span.className = 'panel-fallback-text'; + span.style.fontSize = fontSize; + span.style.lineHeight = lineHeight; + span.textContent = text; + + fallbackBox.appendChild(span); + container.appendChild(fallbackBox); + } + + function appendPanelTypeImage(container, code, imageUrl, options = {}) { + if (!container) return; + + const imageHeight = options.height || '40px'; + const imageWidth = options.width || '40px'; + const objectFit = options.objectFit || 'contain'; + + function createImage(src) { + const img = document.createElement('img'); + img.src = src; + img.alt = code || ''; + img.style.maxHeight = '100%'; + img.style.maxWidth = '100%'; + img.style.height = imageHeight; + img.style.width = imageWidth; + img.style.objectFit = objectFit; + return img; + } + + const candidates = buildPanelTypeImageCandidates(code); + let candidateIndex = 0; + + function tryFallbackCandidate() { + const src = candidates[candidateIndex++]; + if (!src) { + renderPanelTextFallback(container, code, options); + return; + } + + const fallbackImg = createImage(src); + fallbackImg.onerror = tryFallbackCandidate; + container.innerHTML = ''; + container.appendChild(fallbackImg); + } + + if (imageUrl) { + const img = createImage(imageUrl); + img.onerror = tryFallbackCandidate; + container.innerHTML = ''; + container.appendChild(img); + return; + } + + tryFallbackCandidate(); + } + + global.normalizePanelTypeCodeForImage = normalizePanelTypeCodeForImage; + global.buildPanelTypeImageCandidates = buildPanelTypeImageCandidates; + global.renderPanelTextFallback = renderPanelTextFallback; + global.appendPanelTypeImage = appendPanelTypeImage; + global.SignPanelsShared = { getRenderedPanelHeight, calculatePanelOffsets, @@ -392,5 +554,9 @@ handleRoadSignStyleImageMissing, refreshPanelOffsetsForSource, createPanelOffsetRefreshScheduler, + normalizePanelTypeCodeForImage, + buildPanelTypeImageCandidates, + renderPanelTextFallback, + appendPanelTypeImage, }; })(window); diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js index 325f89b..edcf412 100644 --- a/loko/assets/static/assets/unified_index.js +++ b/loko/assets/static/assets/unified_index.js @@ -3127,11 +3127,35 @@ function buildPanelTypeImageCandidates(code) { if (!normalizedCode) return []; const upperCode = normalizedCode.toUpperCase(); - const titleCode = upperCode.charAt(0) + upperCode.slice(1).toLowerCase(); - const lowerCode = upperCode.toLowerCase(); - const uniqueCodes = Array.from(new Set([upperCode, titleCode, lowerCode])); - const candidates = []; + const baseSet = new Set([upperCode]); + const strippedPrefix = upperCode.replace(/^[-_]+/, ''); + if (strippedPrefix) { + baseSet.add(strippedPrefix); + baseSet.add(strippedPrefix.replace(/-/g, '_')); + } + + if (upperCode.includes(' ')) { + baseSet.add(upperCode.replace(/\s+/g, '')); + baseSet.add(upperCode.replace(/\s+/g, '_')); + } + + if (upperCode.includes('-')) { + baseSet.add(upperCode.replace(/-/g, '_')); + } + + const uniqueCodes = []; + baseSet.forEach(item => { + if (!item) return; + const up = item.toUpperCase(); + const title = up.charAt(0) + up.slice(1).toLowerCase(); + const low = up.toLowerCase(); + [up, title, low].forEach(c => { + if (!uniqueCodes.includes(c)) uniqueCodes.push(c); + }); + }); + + const candidates = []; uniqueCodes.forEach(item => { candidates.push(`/static/assets/road_signs/2025/${encodeURIComponent(item)}.svg`); }); @@ -3142,6 +3166,55 @@ function buildPanelTypeImageCandidates(code) { return candidates; } +function renderPanelTextFallback(container, code, options = {}) { + if (!container) return; + const text = String(code || '').trim(); + if (!text) { + container.innerHTML = ''; + return; + } + + const len = text.length; + let fontSize; + let lineHeight = '1.1'; + + if (len <= 4) { + fontSize = '0.72rem'; + lineHeight = '1.15'; + } else if (len <= 8) { + fontSize = '0.62rem'; + lineHeight = '1.1'; + } else if (len <= 14) { + fontSize = '0.54rem'; + lineHeight = '1.05'; + } else if (len <= 24) { + fontSize = '0.48rem'; + lineHeight = '1.05'; + } else { + fontSize = '0.42rem'; + lineHeight = '1.0'; + } + + const isSmallThumb = options.width && parseInt(options.width, 10) < 35; + if (isSmallThumb) { + fontSize = len <= 4 ? '0.55rem' : '0.40rem'; + } + + container.innerHTML = ''; + const fallbackBox = document.createElement('div'); + fallbackBox.className = 'panel-text-fallback'; + fallbackBox.title = text; + + const span = document.createElement('span'); + span.className = 'panel-fallback-text'; + span.style.fontSize = fontSize; + span.style.lineHeight = lineHeight; + span.textContent = text; + + fallbackBox.appendChild(span); + container.appendChild(fallbackBox); +} + function appendPanelTypeImage(container, code, imageUrl, options = {}) { if (!container) return; @@ -3167,7 +3240,7 @@ function appendPanelTypeImage(container, code, imageUrl, options = {}) { function tryFallbackCandidate() { const src = candidates[candidateIndex++]; if (!src) { - container.textContent = code || ''; + renderPanelTextFallback(container, code, options); return; } @@ -3233,6 +3306,8 @@ function renderSignPanelTypeConditionalFilter(thematicCode, def, selectedValues) #accordion-sign-panel-types .panel-item { transition: all 0.2s cubic-bezier(0.165, 0.84, 0.44, 1); border: 1px solid #dee2e6 !important; + overflow: hidden !important; + box-sizing: border-box !important; } #accordion-sign-panel-types .panel-item:hover { transform: translateY(-3px); @@ -3242,6 +3317,55 @@ function renderSignPanelTypeConditionalFilter(thematicCode, def, selectedValues) #accordion-sign-panel-types .panel-item.bg-primary { box-shadow: 0 4px 12px rgba(13, 110, 253, 0.15) !important; } + #accordion-sign-panel-types .panel-img-placeholder { + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; + box-sizing: border-box !important; + padding: 2px !important; + } + #accordion-sign-panel-types .panel-text-fallback, + .panel-text-fallback { + width: 100%; + height: 100%; + max-height: 44px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; + border: 1px dashed #d0d5dd; + border-radius: 4px; + padding: 2px 3px; + box-sizing: border-box; + overflow: hidden; + transition: background-color 0.15s ease, border-color 0.15s ease; + } + #accordion-sign-panel-types .panel-fallback-text, + .panel-fallback-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + line-clamp: 4; + max-height: 40px; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-word; + overflow-wrap: anywhere; + text-align: center; + font-weight: 600; + color: #495057; + letter-spacing: -0.2px; + } + #accordion-sign-panel-types .panel-item.bg-primary .panel-text-fallback, + #accordion-sign-panel-types .panel-item.selected .panel-text-fallback { + background-color: rgba(255, 255, 255, 0.2) !important; + border-color: rgba(255, 255, 255, 0.4) !important; + } + #accordion-sign-panel-types .panel-item.bg-primary .panel-fallback-text, + #accordion-sign-panel-types .panel-item.selected .panel-fallback-text { + color: #ffffff !important; + } `; @@ -3257,11 +3381,11 @@ function renderSignPanelTypeConditionalFilter(thematicCode, def, selectedValues) data-id="${escapeHtml(option.value)}" data-code="${escapeHtml(option.code)}" data-image-url="${escapeHtml(option.imageUrl)}" - style="cursor: pointer; width: 70px;" + style="cursor: pointer; width: 70px; overflow: hidden; box-sizing: border-box;" title="${escapeHtml(title)}">
-
+ style="height: 50px; width: 100%; display: flex; align-items: center; justify-content: center; overflow: hidden; box-sizing: border-box;">
+
${escapeHtml(option.code)}
@@ -3373,21 +3497,24 @@ function initSignPanelTypePickers() { item.className = 'selected-panel-item d-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1'; const content = document.createElement('div'); - content.className = 'd-flex align-items-center'; + content.className = 'd-flex align-items-center text-truncate me-2'; + content.style.maxWidth = 'calc(100% - 30px)'; const thumb = document.createElement('div'); - thumb.className = 'd-flex align-items-center justify-content-center me-2'; + thumb.className = 'd-flex align-items-center justify-content-center me-2 flex-shrink-0'; thumb.style.width = '28px'; thumb.style.height = '28px'; + thumb.style.overflow = 'hidden'; appendPanelTypeImage(thumb, code, imageUrl, { width: '28px', height: '28px' }); const label = document.createElement('span'); - label.className = 'small fw-bold ms-1'; + label.className = 'small fw-bold ms-1 text-truncate'; + label.title = code; label.textContent = code; const removeBtn = document.createElement('button'); removeBtn.type = 'button'; - removeBtn.className = 'btn btn-sm btn-link text-danger p-0 me-1 remove-panel-type'; + removeBtn.className = 'btn btn-sm btn-link text-danger p-0 me-1 remove-panel-type flex-shrink-0'; removeBtn.dataset.id = id; removeBtn.innerHTML = ''; diff --git a/loko/sign/static/sign/sign.js b/loko/sign/static/sign/sign.js index 93e7416..96a7a86 100644 --- a/loko/sign/static/sign/sign.js +++ b/loko/sign/static/sign/sign.js @@ -2370,27 +2370,41 @@ document.addEventListener('DOMContentLoaded', function() { const imageUrl = panelItem.dataset.imageUrl; const panelCode = panelItem.dataset.code; - if (imageUrl) { - imageHtml = ``; + const thumbContainer = document.createElement('div'); + thumbContainer.style.width = '40px'; + thumbContainer.style.height = '40px'; + thumbContainer.style.marginRight = '8px'; + thumbContainer.style.display = 'flex'; + thumbContainer.style.alignItems = 'center'; + thumbContainer.style.justifyContent = 'center'; + thumbContainer.style.overflow = 'hidden'; + thumbContainer.style.flexShrink = '0'; + + if (typeof appendPanelTypeImage === 'function') { + appendPanelTypeImage(thumbContainer, panelCode, imageUrl, { width: '40px', height: '40px' }); + } else if (imageUrl) { + thumbContainer.innerHTML = ``; } else if (typeof buildPanelIconElement === 'function') { const img = buildPanelIconElement(panelCode); img.style.height = '40px'; img.style.width = '40px'; - img.style.marginRight = '8px'; - const tempDiv = document.createElement('div'); - tempDiv.appendChild(img); - imageHtml = tempDiv.innerHTML; + thumbContainer.appendChild(img); + } else if (typeof renderPanelTextFallback === 'function') { + renderPanelTextFallback(thumbContainer, panelCode, { width: '40px', height: '40px' }); } else { - imageHtml = `${panelCode}`; + thumbContainer.innerHTML = `${panelCode}`; } + const tempWrapper = document.createElement('div'); + tempWrapper.appendChild(thumbContainer); + imageHtml = tempWrapper.innerHTML; } const item = document.createElement('div'); item.className = 'd-flex align-items-center bg-white border rounded p-2'; item.innerHTML = ` ${imageHtml} - ${code} - `; @@ -2423,7 +2437,9 @@ document.addEventListener('DOMContentLoaded', function() { const imageUrl = item.dataset.imageUrl; const code = item.dataset.code; - if (imageUrl) { + if (typeof appendPanelTypeImage === 'function') { + appendPanelTypeImage(placeholder, code, imageUrl); + } else if (imageUrl) { const img = document.createElement('img'); img.src = imageUrl; img.style.maxHeight = '100%'; @@ -2434,6 +2450,8 @@ document.addEventListener('DOMContentLoaded', function() { img.style.height = '40px'; img.style.width = '40px'; placeholder.appendChild(img); + } else if (typeof renderPanelTextFallback === 'function') { + renderPanelTextFallback(placeholder, code); } else { placeholder.textContent = code; } diff --git a/loko/sign/templates/sign/index.html b/loko/sign/templates/sign/index.html index 8109433..4b7475e 100644 --- a/loko/sign/templates/sign/index.html +++ b/loko/sign/templates/sign/index.html @@ -124,6 +124,8 @@ border: 1px solid #e9ecef !important; border-radius: 6px !important; background-color: #ffffff !important; + overflow: hidden !important; + box-sizing: border-box !important; } .panel-item:hover, .panel-item-create:hover { @@ -137,6 +139,65 @@ background-color: rgba(13, 110, 253, 0.05) !important; box-shadow: 0 4px 12px rgba(13, 110, 253, 0.1) !important; } + + .panel-img-placeholder { + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; + box-sizing: border-box !important; + padding: 2px !important; + } + + .panel-text-fallback { + width: 100%; + height: 100%; + max-height: 44px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; + border: 1px dashed #d0d5dd; + border-radius: 4px; + padding: 2px 3px; + box-sizing: border-box; + overflow: hidden; + transition: background-color 0.15s ease, border-color 0.15s ease; + } + + .panel-fallback-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + line-clamp: 4; + max-height: 40px; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-word; + overflow-wrap: anywhere; + text-align: center; + font-weight: 600; + color: #495057; + letter-spacing: -0.2px; + } + + .panel-item.selected .panel-text-fallback, + .panel-item-create.selected .panel-text-fallback, + .panel-item.bg-primary .panel-text-fallback, + .panel-item-create.bg-primary .panel-text-fallback { + background-color: rgba(13, 110, 253, 0.12) !important; + border-color: rgba(13, 110, 253, 0.35) !important; + } + + .panel-item.selected .panel-fallback-text, + .panel-item-create.selected .panel-fallback-text { + color: #0d6efd !important; + } + + .panel-item.bg-primary .panel-fallback-text, + .panel-item-create.bg-primary .panel-fallback-text { + color: #ffffff !important; + } {% endblock head %} @@ -306,12 +367,12 @@ data-code="{{ panel.code }}" data-group="{{ group_key }}" data-image-url="{{ panel.image_url|default:'' }}" - style="cursor: pointer; width: 68px; transition: transform 0.15s ease;" + style="cursor: pointer; width: 68px; overflow: hidden; box-sizing: border-box; transition: transform 0.15s ease;" title="{{ panel.code }} - {{ panel.name }}"> -
+
-
{{ panel.code }}
+
{{ panel.code }}
{% endfor %} {% endif %} @@ -2016,7 +2077,9 @@ document.addEventListener('DOMContentLoaded', function() { if (placeholder && placeholder.children.length === 0) { const imageUrl = item.dataset.imageUrl; const code = item.dataset.code; - if (imageUrl) { + if (typeof appendPanelTypeImage === 'function') { + appendPanelTypeImage(placeholder, code, imageUrl); + } else if (imageUrl) { const img = document.createElement('img'); img.src = imageUrl; img.style.maxHeight = '100%'; @@ -2027,6 +2090,8 @@ document.addEventListener('DOMContentLoaded', function() { img.style.height = '40px'; img.style.width = '40px'; placeholder.appendChild(img); + } else if (typeof renderPanelTextFallback === 'function') { + renderPanelTextFallback(placeholder, code); } else { placeholder.textContent = code; } @@ -2177,8 +2242,8 @@ document.addEventListener('DOMContentLoaded', function() { const item = document.createElement('div'); item.className = 'd-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1 shadow-sm'; item.innerHTML = ` - ${code} - `; @@ -2195,8 +2260,8 @@ document.addEventListener('DOMContentLoaded', function() { const item = document.createElement('div'); item.className = 'd-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1 shadow-sm'; item.innerHTML = ` - ${code} - `; diff --git a/loko/sign/templates/sign/sign_panel_form.html b/loko/sign/templates/sign/sign_panel_form.html index ce70fe3..7ef1e47 100644 --- a/loko/sign/templates/sign/sign_panel_form.html +++ b/loko/sign/templates/sign/sign_panel_form.html @@ -85,6 +85,8 @@ border: 1px solid #e9ecef !important; border-radius: 6px !important; background-color: #ffffff !important; + overflow: hidden !important; + box-sizing: border-box !important; } .panel-item:hover, .panel-item-create:hover { @@ -92,6 +94,62 @@ border-color: #0d6efd !important; box-shadow: 0 4px 12px rgba(13, 110, 253, 0.15) !important; } + + .panel-img-placeholder { + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; + box-sizing: border-box !important; + padding: 2px !important; + } + + .panel-text-fallback { + width: 100%; + height: 100%; + max-height: 44px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; + border: 1px dashed #d0d5dd; + border-radius: 4px; + padding: 2px 3px; + box-sizing: border-box; + overflow: hidden; + transition: background-color 0.15s ease, border-color 0.15s ease; + } + + .panel-fallback-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + line-clamp: 4; + max-height: 40px; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-word; + overflow-wrap: anywhere; + text-align: center; + font-weight: 600; + color: #495057; + letter-spacing: -0.2px; + } + + .panel-item.bg-primary .panel-text-fallback, + .panel-item-create.bg-primary .panel-text-fallback, + .panel-item.selected .panel-text-fallback, + .panel-item-create.selected .panel-text-fallback { + background-color: rgba(255, 255, 255, 0.2) !important; + border-color: rgba(255, 255, 255, 0.4) !important; + } + + .panel-item.bg-primary .panel-fallback-text, + .panel-item-create.bg-primary .panel-fallback-text, + .panel-item.selected .panel-fallback-text, + .panel-item-create.selected .panel-fallback-text { + color: #ffffff !important; + } @@ -119,12 +177,12 @@ data-id="{{ panel.id }}" data-code="{{ panel.code }}" data-image-url="{{ panel.image_url|default:'' }}" - style="cursor: pointer; width: 68px; transition: transform 0.15s ease;" + style="cursor: pointer; width: 68px; overflow: hidden; box-sizing: border-box; transition: transform 0.15s ease;" title="{{ panel.code }} - {{ panel.name }}"> -
+
-
{{ panel.code }}
+
{{ panel.code }}
{% endfor %} {% endif %}