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)}">
-