AI and I have been attempting to satisfy this requirement:
- The dashboard 2 page has a grid theme.
- There is a ui-template which may be specified at various sizes eg 6x2 grid units
- Within the template is a div which displays msg.payload. The text is as large as possible to fit within 80% of the widget width and 40% of it's height.
- As the widget display size changes, eg when a phone is rotated or when the list of pages sidebar is shown, the text and div have to adapt too.
[{"id":"22c62ff56e95cf20","type":"group","z":"f8f7abaa42bf3ef9","name":"v2","style":{"label":true},"nodes":["12f5f3d40972dc09","f9df8cf85847ae11","8b4ff721d203aba2","7f27ecc771562dad"],"x":14,"y":219,"w":392,"h":162},{"id":"12f5f3d40972dc09","type":"ui-template","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","group":"bd0dc9d68d230789","page":"","ui":"","name":"Weather Card 4x2","order":2,"width":"4","height":"2","format":"<template>\n <div class=\"dashboard-container\">\n <div class=\"responsive-text\">\n {{ msg.payload }}\n </div>\n <div class=\"dimension-text\"></div>\n </div>\n</template>\n\n<script>\n(function() {\n function updateDimensions(container) {\n const dimText = container.querySelector(\".dimension-text\");\n const respText = container.querySelector(\".responsive-text\");\n if (!dimText || !respText) return;\n\n const { width, height } = container.getBoundingClientRect();\n const maxHeight = height * 0.4;\n const maxWidth = width * 0.75;\n\n // Allow natural height up to maxHeight\n respText.style.height = \"auto\";\n respText.style.maxHeight = maxHeight + \"px\";\n respText.style.maxWidth = maxWidth + \"px\";\n\n // Initial font-size guess\n let newFontSize = Math.floor(maxHeight * 0.8);\n respText.style.fontSize = newFontSize + \"px\";\n respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n\n let textHeight = respText.scrollHeight;\n\n // Too tall → shrink\n if (textHeight > maxHeight) {\n const scaleFactor = maxHeight / textHeight;\n newFontSize = Math.floor(newFontSize * scaleFactor);\n respText.style.fontSize = newFontSize + \"px\";\n respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n }\n\n // Too wide → shrink\n const textWidth = respText.scrollWidth;\n const boxWidth = respText.clientWidth;\n if (textWidth > boxWidth) {\n const scaleFactor = boxWidth / textWidth;\n newFontSize = Math.floor(newFontSize * scaleFactor);\n respText.style.fontSize = newFontSize + \"px\";\n respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n }\n\n // Display widget + div info\n const divRect = respText.getBoundingClientRect();\n dimText.textContent =\n `Widget ${Math.round(width)}px × ${Math.round(height)}px | div: ${Math.round(divRect.width)}px × ${Math.round(divRect.height)}px | font-size: ${newFontSize}px`;\n }\n\n function observeContainers() {\n document.querySelectorAll(\".dashboard-container\").forEach(container => {\n if (!container.__observed) {\n updateDimensions(container);\n\n const ro = new ResizeObserver(() => updateDimensions(container));\n ro.observe(container);\n\n const respText = container.querySelector(\".responsive-text\");\n if (respText) {\n const mo = new MutationObserver(() => updateDimensions(container));\n mo.observe(respText, { childList: true, characterData: true, subtree: true });\n }\n\n container.__observed = true;\n }\n });\n }\n\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", observeContainers);\n } else {\n observeContainers();\n }\n\n setInterval(observeContainers, 500);\n})();\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":290,"y":260,"wires":[[]]},{"id":"f9df8cf85847ae11","type":"ui-template","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","group":"bd0dc9d68d230789","page":"","ui":"","name":"Weather Card 6x4","order":4,"width":6,"height":"4","format":"<template>\n <div class=\"dashboard-container\">\n <div class=\"responsive-text\">\n {{ msg.payload }}\n </div>\n <div class=\"dimension-text\"></div>\n </div>\n</template>\n\n<script>\n(function() {\n function updateDimensions(container) {\n const dimText = container.querySelector(\".dimension-text\");\n const respText = container.querySelector(\".responsive-text\");\n if (!dimText || !respText) return;\n\n const { width, height } = container.getBoundingClientRect();\n const maxHeight = height * 0.4;\n const maxWidth = width * 0.75;\n\n // Allow natural height up to maxHeight\n respText.style.height = \"auto\";\n respText.style.maxHeight = maxHeight + \"px\";\n respText.style.maxWidth = maxWidth + \"px\";\n\n // Initial font-size guess\n let newFontSize = Math.floor(maxHeight * 0.8);\n respText.style.fontSize = newFontSize + \"px\";\n respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n\n let textHeight = respText.scrollHeight;\n\n // Too tall → shrink\n if (textHeight > maxHeight) {\n const scaleFactor = maxHeight / textHeight;\n newFontSize = Math.floor(newFontSize * scaleFactor);\n respText.style.fontSize = newFontSize + \"px\";\n respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n }\n\n // Too wide → shrink\n const textWidth = respText.scrollWidth;\n const boxWidth = respText.clientWidth;\n if (textWidth > boxWidth) {\n const scaleFactor = boxWidth / textWidth;\n newFontSize = Math.floor(newFontSize * scaleFactor);\n respText.style.fontSize = newFontSize + \"px\";\n respText.style.lineHeight = Math.round(newFontSize * 1.05) + \"px\";\n }\n\n // Display widget + div info\n const divRect = respText.getBoundingClientRect();\n dimText.textContent =\n `Widget ${Math.round(width)}px × ${Math.round(height)}px | div: ${Math.round(divRect.width)}px × ${Math.round(divRect.height)}px | font-size: ${newFontSize}px`;\n }\n\n function observeContainers() {\n document.querySelectorAll(\".dashboard-container\").forEach(container => {\n if (!container.__observed) {\n updateDimensions(container);\n\n const ro = new ResizeObserver(() => updateDimensions(container));\n ro.observe(container);\n\n const respText = container.querySelector(\".responsive-text\");\n if (respText) {\n const mo = new MutationObserver(() => updateDimensions(container));\n mo.observe(respText, { childList: true, characterData: true, subtree: true });\n }\n\n container.__observed = true;\n }\n });\n }\n\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", observeContainers);\n } else {\n observeContainers();\n }\n\n setInterval(observeContainers, 500);\n})();\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":290,"y":300,"wires":[[]]},{"id":"8b4ff721d203aba2","type":"ui-template","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","group":"","page":"42e2d917b7e20f69","ui":"","name":"CSS","order":0,"width":0,"height":0,"head":"","format":"/*\n Dashboard Responsive Text Template\n Version: v2\n Variant: responsive-text auto height, max 40%\n*/\n\n.dashboard-container {\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: flex-start;\n height: 100%;\n width: 100%;\n background-color: lemonchiffon;\n border: 3px solid black;\n border-radius: 5px;\n overflow: hidden;\n position: relative;\n padding: 4px;\n box-sizing: border-box;\n}\n\n.responsive-text {\n display: inline-block; /* shrink-to-fit width/height */\n max-width: 75%; /* cap width */\n max-height: 40%; /* cap height */\n border: 2px solid red;\n color: #333;\n font-family: \"Times New Roman\", serif;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n box-sizing: border-box;\n line-height: 1;\n padding-bottom: 0.15em; /* allow room for descenders */\n}\n\n.dimension-text {\n font-size: 0.8rem;\n color: #555;\n margin-top: 4px;\n}\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"page:style","className":"","x":110,"y":340,"wires":[[]]},{"id":"7f27ecc771562dad","type":"ui-button-group","z":"f8f7abaa42bf3ef9","g":"22c62ff56e95cf20","name":"","group":"87928b14d093bc1d","order":2,"width":6,"height":1,"label":"button group","className":"","rounded":true,"useThemeColors":true,"passthru":false,"options":[{"label":"Short","icon":"","value":"Quito","valueType":"str","color":"#009933"},{"label":"Medium","icon":"","value":"České Budějovice","valueType":"str","color":"#999999"},{"label":"Long","icon":"","value":"Antananarivo International Airport","valueType":"str","color":"#ff6666"}],"topic":"topic","topicType":"msg","x":110,"y":280,"wires":[["12f5f3d40972dc09","f9df8cf85847ae11"]]},{"id":"bd0dc9d68d230789","type":"ui-group","name":"v2","page":"42e2d917b7e20f69","width":"10","height":1,"order":2,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"42e2d917b7e20f69","type":"ui-page","name":"v2","ui":"e9c974f7c1d080d1","path":"/v2","icon":"home","layout":"grid","theme":"c757d5c250b7d202","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":true,"disabled":false},{"id":"87928b14d093bc1d","type":"ui-group","name":"Buttons","page":"42e2d917b7e20f69","width":6,"height":1,"order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"e9c974f7c1d080d1","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true,"allowInstall":true},{"id":"c757d5c250b7d202","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"d7d0a35ee5de47a6","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.28.0"}}]
There are some display sizes where the text gets cropped, maybe not ideal but OK.
- This uses a good deal of javascript, which I would prefer to avoid, but chatgpt cannot come up with a fully working version in pure CSS. Can you do better?
- Is there a simpler way to achieve this responsive text in a template widget?