From 08a2d80db074f9a19a2600789de5ad0caa169268 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sat, 9 Sep 2023 12:31:03 +0200 Subject: [PATCH] [BasicUI] Add a new header line for image, chart and video elements. (#2010) Header line for video element contains icon and label. Header line for image element contains icon, label and a button to switch between no upscale and upscale of the image. Header line for chart element contains icon, label and 4 buttons: - one button to show or hide the legend - one button to change the time range - one button to switch between no upscale and upscale of the chart - one button to refresh the chart Fix handling of iconcolor and labelcolor parameters for mapview and webview elements. For image and chart elements, the header line is always present so that user has an access to its buttons. For video, mapview and webview elements, if the label is empty, the header line is hidden. For chart and image elements, there is now no upscale applied by default (tablet/phone devices) but a button allows upscaling. Closes #1939 Fixes #1367 Also related to #1930 Signed-off-by: Laurent Garnier --- .../basic/internal/render/ChartRenderer.java | 41 +++- .../basic/internal/render/ImageRenderer.java | 5 +- .../internal/render/MapviewRenderer.java | 7 + .../basic/internal/render/VideoRenderer.java | 11 +- .../internal/render/WebviewRenderer.java | 7 + .../src/main/resources/snippets/chart.html | 37 ++++ .../src/main/resources/snippets/image.html | 16 ++ .../src/main/resources/snippets/mapview.html | 15 +- .../src/main/resources/snippets/video.html | 9 + .../src/main/resources/snippets/webview.html | 15 +- .../org.openhab.ui.basic/web-src/_layout.scss | 40 +++- .../web-src/_theming.scss | 4 + .../org.openhab.ui.basic/web-src/smarthome.js | 188 +++++++++++++++++- 13 files changed, 365 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ChartRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ChartRenderer.java index 37a03cc8d4..44b59ed30f 100644 --- a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ChartRenderer.java +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ChartRenderer.java @@ -15,10 +15,12 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Date; +import java.util.List; import org.eclipse.emf.common.util.ECollections; import org.eclipse.emf.common.util.EList; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.items.GroupItem; @@ -91,7 +93,9 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th } // if legend parameter is given, add corresponding GET parameter + boolean legend = item instanceof GroupItem && !forceAsItem; if (chart.getLegend() != null) { + legend = chart.getLegend(); if (chart.getLegend()) { chartUrl += "&legend=true"; } else { @@ -123,7 +127,10 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th } String snippet = getSnippet("chart"); - snippet = preprocessSnippet(snippet, w); + snippet = preprocessSnippet(snippet, w, true); + + // Process the color tags + snippet = processColor(w, snippet); if (chart.getRefresh() > 0) { snippet = snippet.replace("%update_interval%", Integer.toString(chart.getRefresh())); @@ -136,6 +143,18 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th snippet = snippet.replace("%valid_url%", "true"); snippet = snippet.replace("%ignore_refresh%", ignoreRefresh ? "true" : "false"); snippet = snippet.replace("%url%", url); + snippet = snippet.replace("%legend%", Boolean.valueOf(legend).toString()); + + List> periods = List.of(List.of("Last hour", "h"), List.of("Last 4 hours", "4h"), + List.of("Last 8 hours", "8h"), List.of("Last 12 hours", "12h"), List.of("Last day", "D"), + List.of("Last 2 days", "2D"), List.of("Last 3 days", "3D"), List.of("Last week", "W"), + List.of("Last 2 weeks", "2W"), List.of("Last month", "M"), List.of("Last 2 months", "2M"), + List.of("Last 4 months", "4M"), List.of("Last year", "Y")); + StringBuilder rowSB = new StringBuilder(); + for (List period : periods) { + buildRow(chart, period.get(0), period.get(1), chart.getPeriod(), rowSB); + } + snippet = snippet.replace("%period_rows%", rowSB.toString()); sb.append(snippet); } catch (ItemNotFoundException e) { @@ -143,4 +162,24 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th } return ECollections.emptyEList(); } + + private void buildRow(Chart w, @Nullable String lab, String cmd, String current, StringBuilder rowSB) + throws RenderException { + String rowSnippet = getSnippet("selection_row"); + + String command = cmd; + String label = lab == null ? cmd : lab; + + rowSnippet = rowSnippet.replace("%item%", w.getItem() != null ? w.getItem() : ""); + rowSnippet = rowSnippet.replace("%cmd%", escapeHtml(command)); + rowSnippet = rowSnippet.replace("%label%", escapeHtml(label)); + + if (command.equals(current)) { + rowSnippet = rowSnippet.replace("%checked%", "checked=\"true\""); + } else { + rowSnippet = rowSnippet.replace("%checked%", ""); + } + + rowSB.append(rowSnippet); + } } diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ImageRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ImageRenderer.java index e6c691e45a..51d6e9d4f9 100644 --- a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ImageRenderer.java +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/ImageRenderer.java @@ -69,7 +69,10 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th String widgetId = itemUIRegistry.getWidgetId(w); snippet = snippet.replace("%id%", widgetId); - snippet = preprocessSnippet(snippet, w); + snippet = preprocessSnippet(snippet, w, true); + + // Process the color tags + snippet = processColor(w, snippet); boolean validUrl = isValidURL(image.getUrl()); String proxiedUrl = "../proxy?sitemap=" + sitemap + "&widgetId=" + widgetId; diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/MapviewRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/MapviewRenderer.java index d900d8168e..13c96c14ee 100644 --- a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/MapviewRenderer.java +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/MapviewRenderer.java @@ -57,7 +57,14 @@ public boolean canRender(Widget w) { public EList renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException { Mapview mapview = (Mapview) w; String snippet = getSnippet("mapview"); + + boolean showHeaderRow = !getLabel(w).isEmpty(); + snippet = snippet.replace("%header_visibility_class%", + showHeaderRow ? "%visibility_class%" : "mdl-form__row--hidden"); + snippet = snippet.replace("%header_row%", Boolean.valueOf(showHeaderRow).toString()); + snippet = preprocessSnippet(snippet, mapview, true); + // Process the color tags snippet = processColor(w, snippet); diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/VideoRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/VideoRenderer.java index ab95955437..58739d4319 100644 --- a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/VideoRenderer.java +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/VideoRenderer.java @@ -66,7 +66,16 @@ public EList renderWidget(Widget w, StringBuilder sb, String sitemap) th && videoWidget.getEncoding().toLowerCase().contains("mjpeg")) ? "image" : "video"; snippet = getSnippet(snippetName); - snippet = preprocessSnippet(snippet, w); + + boolean showHeaderRow = !getLabel(w).isEmpty(); + snippet = snippet.replace("%header_visibility_class%", + showHeaderRow ? "%visibility_class%" : "mdl-form__row--hidden"); + snippet = snippet.replace("%header_row%", Boolean.valueOf(showHeaderRow).toString()); + + snippet = preprocessSnippet(snippet, w, true); + + // Process the color tags + snippet = processColor(w, snippet); State state = itemUIRegistry.getState(w); String url; diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/WebviewRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/WebviewRenderer.java index 649773b424..c24dfb3bad 100644 --- a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/WebviewRenderer.java +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/WebviewRenderer.java @@ -52,7 +52,14 @@ public boolean canRender(Widget w) { public EList renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException { Webview webview = (Webview) w; String snippet = getSnippet("webview"); + + boolean showHeaderRow = !getLabel(w).isEmpty(); + snippet = snippet.replace("%header_visibility_class%", + showHeaderRow ? "%visibility_class%" : "mdl-form__row--hidden"); + snippet = snippet.replace("%header_row%", Boolean.valueOf(showHeaderRow).toString()); + snippet = preprocessSnippet(snippet, webview, true); + // Process the color tags snippet = processColor(w, snippet); diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/chart.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/chart.html index a2b44a5ff7..063fb7e82b 100644 --- a/bundles/org.openhab.ui.basic/src/main/resources/snippets/chart.html +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/chart.html @@ -1,3 +1,38 @@ +
+ + %icon_snippet% + + + %label% + +
+ + Show or hde legend + + Choose time frame + + + Upscale chart or not + + Refresh chart +
+
diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/image.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/image.html index c51025a2bd..98b742eea5 100644 --- a/bundles/org.openhab.ui.basic/src/main/resources/snippets/image.html +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/image.html @@ -1,3 +1,18 @@ +
+ + %icon_snippet% + + + %label% + +
+ + Upscale image or not +
+
diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/mapview.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/mapview.html index dffb31cc72..a78eddc2d1 100644 --- a/bundles/org.openhab.ui.basic/src/main/resources/snippets/mapview.html +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/mapview.html @@ -1,10 +1,10 @@ -
- - %icon_snippet% - - - %label% - +
+ + %icon_snippet% + + + %label% +
diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/video.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/video.html index 257d533fa8..370de2fe28 100644 --- a/bundles/org.openhab.ui.basic/src/main/resources/snippets/video.html +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/video.html @@ -1,8 +1,17 @@ +
+ + %icon_snippet% + + + %label% + +
diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/webview.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/webview.html index aab1f5e96d..7a033b4f9b 100644 --- a/bundles/org.openhab.ui.basic/src/main/resources/snippets/webview.html +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/webview.html @@ -1,16 +1,17 @@ -
- - %icon_snippet% - - - %label% - +
+ + %icon_snippet% + + + %label% +
diff --git a/bundles/org.openhab.ui.basic/web-src/_layout.scss b/bundles/org.openhab.ui.basic/web-src/_layout.scss index e2a08660e7..399833ef8f 100644 --- a/bundles/org.openhab.ui.basic/web-src/_layout.scss +++ b/bundles/org.openhab.ui.basic/web-src/_layout.scss @@ -70,6 +70,9 @@ @include align-items-2011(center); white-space: nowrap; height: $form-row-desktop-height; + html.ui-layout-condensed & { + height: $form-row-desktop-height-condensed; + } box-sizing: content-box; padding: 4px 0; margin: 0 $mdl-grid-spacing; @@ -79,7 +82,7 @@ padding: 4px $mdl-grid-spacing; margin: 0; html.ui-layout-condensed & { - height: $form-row-desktop-height-condensed; + height: $form-row-mobile-height-condensed; } } @media screen and (min-width: $layout-tablet-size-threshold) { @@ -98,12 +101,19 @@ height: auto; } } + &--header { + border-bottom: none; + padding-bottom: 0; + padding-left: 48px; + padding-right: 48px; + @media screen and (max-width: $layout-tablet-size-threshold) { + padding-left: $mdl-grid-spacing + 16px; + padding-right: $mdl-grid-spacing + 16px; + } + } &--hidden { display: none; } - html.ui-layout-condensed & { - height: $form-row-desktop-height-condensed; - } } &__image { &.mdl-form__control { @@ -114,11 +124,7 @@ max-width: 100%; } } - @media screen and (max-width: $layout-tablet-size-threshold) { - &.mdl-form__control { - padding-left: 0; - padding-right: 0; - } + &-upscale { width: 100%; img { height: auto; @@ -264,6 +270,22 @@ padding-top: 6px; } } + &__header { + padding-left: 8px; + padding-right: $form-row-desktop-padding; + @media screen and (max-width: $layout-tablet-size-threshold) { + padding-right: $form-row-mobile-padding; + } + .mdl-button { + box-shadow: none; + -webkit-box-shadow: none; + min-width: 0; + padding-top: 6px; + } + &-rows { + display: none; + } + } &__group { padding-top: 4px; } diff --git a/bundles/org.openhab.ui.basic/web-src/_theming.scss b/bundles/org.openhab.ui.basic/web-src/_theming.scss index b4d1d63d01..99f6e4609f 100644 --- a/bundles/org.openhab.ui.basic/web-src/_theming.scss +++ b/bundles/org.openhab.ui.basic/web-src/_theming.scss @@ -84,6 +84,10 @@ body { color: var(--container-text-color, #616161) !important; } +.mdl-form__row--header { + border-bottom: none; +} + .mdl-switch .mdl-switch__track { background: rgba(0,0,0,.26); background: var(--switch-off-track-bg, rgba(0,0,0,.26)); diff --git a/bundles/org.openhab.ui.basic/web-src/smarthome.js b/bundles/org.openhab.ui.basic/web-src/smarthome.js index 58c74243f7..83e800aa9d 100644 --- a/bundles/org.openhab.ui.basic/web-src/smarthome.js +++ b/bundles/org.openhab.ui.basic/web-src/smarthome.js @@ -358,10 +358,19 @@ } _t.item = _t.parentNode.getAttribute(o.itemAttribute); _t.id = _t.parentNode.getAttribute(o.idAttribute); - _t.iconContainer = _t.formRow.querySelector(o.formIcon); - _t.icon = _t.formRow.querySelector(o.formIconImg); _t.visible = !_t.formRow.classList.contains(o.formRowHidden); - _t.label = _t.formRow.querySelector(o.formLabel); + _t.headerRow = _t.parentNode.getAttribute("data-header-row"); + if (_t.headerRow !== null) { + _t.formHeaderRow = _t.formRow.previousElementSibling; + _t.iconContainer = _t.formHeaderRow.querySelector(o.formIcon); + _t.icon = _t.formHeaderRow.querySelector(o.formIconImg); + _t.label = _t.formHeaderRow.querySelector(o.formLabel); + } else { + _t.formHeaderRow = null; + _t.iconContainer = _t.formRow.querySelector(o.formIcon); + _t.icon = _t.formRow.querySelector(o.formIconImg); + _t.label = _t.formRow.querySelector(o.formLabel); + } function convertToInlineSVG() { this.removeEventListener("load", convertToInlineSVG); @@ -406,7 +415,11 @@ // Replace the current icon element with the built inline SVG _t.iconContainer.replaceChild(newIconElement, _t.icon); - _t.icon = _t.parentNode.parentNode.querySelector(o.formIconSvg); + if (_t.headerRow !== null) { + _t.icon = _t.formHeaderRow.querySelector(o.formIconSvg); + } else { + _t.icon = _t.formRow.querySelector(o.formIconSvg); + } }; _t.getSVGIconAndReplaceWithInline = function(srcUrl, checkCurrentColor, defaultSVG) { @@ -458,8 +471,14 @@ _t.setVisible = function(state) { if (state) { + if (_t.headerRow === "true") { + _t.formHeaderRow.classList.remove(o.formRowHidden); + } _t.formRow.classList.remove(o.formRowHidden); } else { + if (_t.headerRow === "true") { + _t.formHeaderRow.classList.add(o.formRowHidden); + } _t.formRow.classList.add(o.formRowHidden); } @@ -555,6 +574,12 @@ } else { this.parentNode = parentNode; this.id = this.parentNode.getAttribute(o.idAttribute); + this.headerRow = this.parentNode.getAttribute("data-header-row"); + if (this.headerRow !== null) { + this.formHeaderRow = this.parentNode.parentNode.previousElementSibling; + } else { + this.formHeaderRow = null; + } } var @@ -568,6 +593,27 @@ _t.url = parentNode.getAttribute("data-proxied-url"); _t.validUrl = parentNode.getAttribute("data-valid-url") === "true"; _t.ignoreRefresh = parentNode.getAttribute("data-ignore-refresh") === "true"; + _t.legendButton = null; + _t.periodButton = null; + _t.upscaleButton = null; + _t.refreshButton = null; + _t.displayLegend = _t.parentNode.getAttribute("data-legend") === "true"; + _t.period = null; + _t.upscale = false; + + if (_t.headerRow === "true") { + _t.legendButton = _t.formHeaderRow.querySelector(o.image.legendButton); + _t.periodButton = _t.formHeaderRow.querySelector(o.image.periodButton); + _t.upscaleButton = _t.formHeaderRow.querySelector(o.image.upscaleButton); + _t.refreshButton = _t.formHeaderRow.querySelector(o.image.refreshButton); + if (_t.legendButton !== null) { + if (_t.displayLegend) { + _t.legendButton.classList.add(o.buttonActiveClass); + } else { + _t.legendButton.classList.remove(o.buttonActiveClass); + } + } + } _t.setValuePrivate = function(value, itemState, visible) { if (!visible) { @@ -591,9 +637,15 @@ _t.setVisible = function(state) { if (state) { + if (_t.headerRow === "true") { + _t.formHeaderRow.classList.remove(o.formRowHidden); + } _t.formRow.classList.remove(o.formRowHidden); _t.activateRefresh(); } else { + if (_t.headerRow === "true") { + _t.formHeaderRow.classList.add(o.formRowHidden); + } _t.formRow.classList.add(o.formRowHidden); _t.deactivateRefresh(); } @@ -628,14 +680,134 @@ }, _t.updateInterval); }; + function onLegendClick() { + _t.displayLegend = !_t.displayLegend; + if (_t.displayLegend) { + _t.legendButton.classList.add(o.buttonActiveClass); + } else { + _t.legendButton.classList.remove(o.buttonActiveClass); + } + _t.url = _t.url.replace(/&legend=(true|false)/, ""); + if (_t.displayLegend) { + _t.url = _t.url + "&legend=true"; + } else { + _t.url = _t.url + "&legend=false"; + } + _t.image.setAttribute("src", _t.url + "&t=" + Date.now()); + } + + function onPeriodChange(event) { + event.stopPropagation(); + + if (event.target.tagName.toLowerCase() !== "input") { + return; + } + + _t.period = event.target.getAttribute("value"); + _t.url = _t.url.replace(/&period=(h|4h|8h|12h|D|2D|3D|W|2W|M|2M|4M|Y)/, "&period=" + _t.period); + _t.image.setAttribute("src", _t.url + "&t=" + Date.now()); + + setTimeout(function() { + _t.modalPeriods.hide(); + }, 300); + } + + _t.showModalPeriods = function() { + var + content = _t.formHeaderRow.querySelector(o.image.periodRows).innerHTML; + + _t.modalPeriods = new Modal(content); + _t.modalPeriods.show(); + _t.modalPeriods.onHide = function() { + var + items = [].slice.call(_t.modalPeriods.container.querySelectorAll(o.formRadio)); + + componentHandler.downgradeElements(items); + items.forEach(function(control) { + control.removeEventListener("click", onPeriodChange); + }); + + _t.modalPeriods = null; + }; + + var + controls = [].slice.call(_t.modalPeriods.container.querySelectorAll(o.formRadio)); + + if (_t.period !== null) { + var + items = [].slice.call(_t.modalPeriods.container.querySelectorAll("input[type=radio]")); + + items.forEach(function(radioItem) { + if (radioItem.value === _t.period) { + radioItem.checked = true; + } else { + radioItem.checked = false; + } + }); + } + + controls.forEach(function(control) { + componentHandler.upgradeElement(control, "MaterialRadio"); + control.addEventListener("click", onPeriodChange); + }); + }; + + function onUpscaleClick() { + _t.upscale = !_t.upscale; + if (_t.upscale) { + _t.upscaleButton.classList.add(o.buttonActiveClass); + } else { + _t.upscaleButton.classList.remove(o.buttonActiveClass); + } + if (_t.upscale) { + _t.parentNode.classList.add(o.image.upscaleClass); + } else { + _t.parentNode.classList.remove(o.image.upscaleClass); + } + } + + function onRefreshClick() { + _t.image.setAttribute("src", _t.url + "&t=" + Date.now()); + } + _t.destroy = function() { var imageParent = _t.image.parentNode; _t.image.setAttribute("src", urlNoneIcon); imageParent.removeChild(_t.image); + + if (_t.legendButton !== null) { + componentHandler.downgradeElements([ _t.legendButton ]); + _t.legendButton.removeEventListener("click", onLegendClick); + } + if (_t.periodButton !== null) { + componentHandler.downgradeElements([ _t.periodButton ]); + _t.periodButton.removeEventListener("click", _t.showModalPeriods); + } + if (_t.upscaleButton !== null) { + componentHandler.downgradeElements([ _t.upscaleButton ]); + _t.upscaleButton.removeEventListener("click", onUpscaleClick); + } + if (_t.refreshButton !== null) { + componentHandler.downgradeElements([ _t.refreshButton ]); + _t.refreshButton.removeEventListener("click", onRefreshClick); + } }; + if (_t.legendButton !== null) { + _t.legendButton.addEventListener("click", onLegendClick); + } + if (_t.periodButton !== null) { + _t.periodButton.addEventListener("click", _t.showModalPeriods); + } + if (_t.upscaleButton !== null) { + _t.upscaleButton.addEventListener("click", onUpscaleClick); + } + if (_t.refreshButton !== null) { + _t.refreshButton.addEventListener("click", onRefreshClick); + } + if (_t.visible) { _t.activateRefresh(); } @@ -2783,6 +2955,14 @@ colorpicker: ".colorpicker", button: ".colorpicker__buttons > button" }, + image: { + legendButton: ".chart-legend-button", + periodButton: ".chart-period-button", + periodRows: ".mdl-form__header-rows", + upscaleButton: ".image-upscale-button", + upscaleClass: "mdl-form__image-upscale", + refreshButton: ".chart-refresh-button" + }, notify: ".mdl-notify__container", notifyHidden: "mdl-notify--hidden", notifyTemplateOffline: "template-offline-notify",