From 4e74d056d0cba89955430090ccc707e16babb3de Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Sun, 9 Apr 2023 00:19:14 +0200 Subject: [PATCH] [basicui] input widget (#1729) Depends on [PR #3398](https://github.com/openhab/openhab-core/pull/3398) This adds support for an input widget in Basic UI. See Core PR for more extensive description. --------- Signed-off-by: Mark Herwege --- .../basic/internal/render/InputRenderer.java | 95 ++++++++++++++ .../src/main/resources/snippets/input.html | 26 ++++ .../org.openhab.ui.basic/web-src/_layout.scss | 22 ++++ .../web-src/_theming.scss | 9 ++ .../org.openhab.ui.basic/web-src/smarthome.js | 122 ++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/InputRenderer.java create mode 100644 bundles/org.openhab.ui.basic/src/main/resources/snippets/input.html diff --git a/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/InputRenderer.java b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/InputRenderer.java new file mode 100644 index 000000000..43711c1d3 --- /dev/null +++ b/bundles/org.openhab.ui.basic/src/main/java/org/openhab/ui/basic/internal/render/InputRenderer.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.ui.basic.internal.render; + +import org.eclipse.emf.common.util.ECollections; +import org.eclipse.emf.common.util.EList; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.model.sitemap.sitemap.Input; +import org.openhab.core.model.sitemap.sitemap.Widget; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.ui.items.ItemUIRegistry; +import org.openhab.ui.basic.render.RenderException; +import org.openhab.ui.basic.render.WidgetRenderer; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an implementation of the {@link WidgetRenderer} interface, which + * can produce HTML code for Input widgets. + * + * @author Mark Herwege - Initial contribution + */ +@Component(service = WidgetRenderer.class) +@NonNullByDefault +public class InputRenderer extends AbstractWidgetRenderer { + + private final Logger logger = LoggerFactory.getLogger(InputRenderer.class); + + @Activate + public InputRenderer(final BundleContext bundleContext, final @Reference TranslationProvider i18nProvider, + final @Reference ItemUIRegistry itemUIRegistry, final @Reference LocaleProvider localeProvider) { + super(bundleContext, i18nProvider, itemUIRegistry, localeProvider); + } + + @Override + public boolean canRender(Widget w) { + return w instanceof Input; + } + + @Override + public EList renderWidget(Widget w, StringBuilder sb, String sitemap) throws RenderException { + String snippet = getSnippet("input"); + + snippet = preprocessSnippet(snippet, w); + + String dataState = getValue(w); + State state = itemUIRegistry.getState(w); + if (state == null || state instanceof UnDefType) { + snippet = snippet.replace("%undef_state%", dataState); + snippet = snippet.replace("%data_state%", ""); + } else { + snippet = snippet.replace("%undef_state%", ""); + snippet = snippet.replace("%data_state%", dataState); + } + + Item item = null; + try { + item = itemUIRegistry.getItem(w.getItem()); + } catch (ItemNotFoundException e) { + logger.debug("Failed to retrieve item during widget rendering: {}", e.getMessage()); + } + String dataType; + if (item != null && item.getAcceptedDataTypes().stream().anyMatch(o -> Number.class.isAssignableFrom(o))) { + dataType = "Number"; + } else { + dataType = "Text"; + } + snippet = snippet.replace("%data_type%", dataType); + + // Process the color tags + snippet = processColor(w, snippet); + + sb.append(snippet); + return ECollections.emptyEList(); + } +} diff --git a/bundles/org.openhab.ui.basic/src/main/resources/snippets/input.html b/bundles/org.openhab.ui.basic/src/main/resources/snippets/input.html new file mode 100644 index 000000000..fc751439b --- /dev/null +++ b/bundles/org.openhab.ui.basic/src/main/resources/snippets/input.html @@ -0,0 +1,26 @@ +
+ + %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 e07466ec9..c2e78c29c 100644 --- a/bundles/org.openhab.ui.basic/web-src/_layout.scss +++ b/bundles/org.openhab.ui.basic/web-src/_layout.scss @@ -217,6 +217,25 @@ box-shadow: none; -webkit-box-shadow: none; } + .mdl-textfield { + &__input, + &__label { + font-family: "Helvetica","Arial",sans-serif; + font-size: 14px; + text-align: right; + } + &__input { + font-weight: 700; + } + &__label { + margin: 0 -16px; + padding: 0 16px 0 0; + @media screen and (max-width: $layout-tablet-size-threshold) { + margin: 0 -12px; + padding: 0 12px 0 0; + } + } + } } &__setpoint, &__colorpicker, @@ -244,6 +263,9 @@ &__slider { width: 140px; } + &__input { + width: 60%; + } &__title { margin: 0; padding-left: $form-row-desktop-padding + $mdl-grid-spacing; diff --git a/bundles/org.openhab.ui.basic/web-src/_theming.scss b/bundles/org.openhab.ui.basic/web-src/_theming.scss index 3aa45e750..bc33de1bf 100644 --- a/bundles/org.openhab.ui.basic/web-src/_theming.scss +++ b/bundles/org.openhab.ui.basic/web-src/_theming.scss @@ -7,6 +7,7 @@ body[data-theme="default"] { --container-text-color: #616161; --switch-on-track-bg: #9fa8da; --border-color: #ccc; + --input-undef-color: rgba(0,0,0,.26); } body[data-theme="dark"] { @@ -18,6 +19,7 @@ body[data-theme="dark"] { --container-text-color: #c0c0c0; --switch-on-track-bg: #9fa8da; --border-color: #343434; + --input-undef-color: rgba(158,158,158,.26); } .mdl-layout__header { @@ -54,3 +56,10 @@ body { background: #3f51b5; background: var(--primary-color, #3f51b5); } + +.mdl-textfield__input { + border-bottom: 1px solid var(--border-color, #ccc); +} +.mdl-textfield__label { + color: var(--input-undef-color, 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 bfd951203..4ff6ffc47 100644 --- a/bundles/org.openhab.ui.basic/web-src/smarthome.js +++ b/bundles/org.openhab.ui.basic/web-src/smarthome.js @@ -2,6 +2,7 @@ * Eclipse SmartHome BasicUI javascript * * @author Vlad Ivanov — initial version + * @author Mark Herwege - input widget */ /*eslint-env browser */ @@ -259,6 +260,27 @@ }; } + function WaitingTimer(callback, waitingTime) { + var + _t = this, + timeoutId = null, + args; + + _t.wait = function() { + args = arguments; + timeoutId = setTimeout(function() { + callback.apply(null, args); + this.timeoutId = undefined; + }, waitingTime); + }; + + _t.cancel = function() { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + }; + } + function DebounceProxy(callback, callInterval) { var _t = this, @@ -1510,6 +1532,102 @@ _t.input.addEventListener("change", onChange); } + /* class ControlInput extends Control */ + function ControlInput(parentNode) { + Control.call(this, parentNode); + + var + _t = this; + + _t.input = _t.parentNode.querySelector("input[type=text]"); + _t.itemType = _t.parentNode.getAttribute(o.itemTypeAttribute); + _t.verify = undefined; + + var + lastValue = _t.input.value, + lastItemState, + numberPattern = /^(\+|-)?[0-9\.,]+/, + dotSeparatorPattern = /^-?(([0-9]{1,3}(,[0-9]{3})*)|([0-9]*))?(\.[0-9]+)?$/, + commaSeparatorPattern = /^-?(([0-9]{1,3}(\.[0-9]{3})*)|([0-9]*))?(,[0-9]+)?$/; + + if (_t.input.nextElementSibling.innerHTML.trim() === "") { + lastItemState = lastValue; + } else { + lastValue = _t.input.nextElementSibling.innerHTML.trim(); + lastItemState = "NULL"; + } + + function onChange() { + var + changeValue = _t.input.value, + changed = true; + if (_t.itemType === "Number") { + changeValue = changeValue.trim(); + var numberValue = changeValue.match(numberPattern); + if (!numberValue || numberValue.length < 1) { + changed = false; + } else { + var unitValue = changeValue.substring(numberValue[0].length).trim(); + changeValue = numberValue[0].replace(/^\+/, ""); + if (commaSeparatorPattern.test(changeValue) && !dotSeparatorPattern.test(changeValue)) { + changeValue = changeValue.replace(/\./g, "").replace(",", "."); + } + if (unitValue.length > 1) { + changeValue = changeValue + " " + unitValue; + } + } + } + + if (!changed) { + _t.setValuePrivate(lastValue, lastItemState); + } else { + _t.parentNode.dispatchEvent(createEvent("control-change", { + item: _t.item, + value: changeValue + })); + // We don't know if the sent value is a valid command and will update the item state. + // If we don't receive an update in 1s, revert to the previous value. + _t.verify = new WaitingTimer(function() { + _t.setValuePrivate(lastValue, lastItemState); + }, 1000); + _t.verify.wait(); + } + } + + _t.setValuePrivate = function(value, itemState) { + if (_t.verify) { + _t.verify.cancel(); + } + + var newValue = value; + var undefValue = ""; + if (itemState === "undefined" || itemState === "NULL" || itemState === "UNDEF") { + newValue = ""; + undefValue = value; + } + + _t.input.value = newValue; + _t.input.nextElementSibling.innerHTML = undefValue; + + _t.input.parentNode.MaterialTextfield.change(); + _t.input.parentNode.MaterialTextfield.checkValidity(); + + lastValue = value; + lastItemState = itemState; + }; + + _t.setValueColor = function(color) { + _t.input.style.color = color; + }; + + _t.destroy = function() { + _t.input.removeEventListener("change", onChange); + componentHandler.downgradeElements([ _t.parentNode ]); + }; + + _t.input.addEventListener("change", onChange); + } + /* class ControlSlider extends Control */ function ControlSlider(parentNode) { Control.call(this, parentNode); @@ -1877,6 +1995,9 @@ case "text": appendControl(new ControlText(e)); break; + case "input": + appendControl(new ControlInput(e)); + break; case "colorpicker": appendControl(new ControlColorpicker(e)); break; @@ -2424,6 +2545,7 @@ }); })({ itemAttribute: "data-item", + itemTypeAttribute: "data-item-type", idAttribute: "data-widget-id", iconAttribute: "data-icon", iconTypeAttribute: "data-icon-type",