[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 <mark.herwege@telenet.be>
pull/1833/head
Mark Herwege 2023-04-09 00:19:14 +02:00 committed by GitHub
parent 4edde23af8
commit 4e74d056d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 274 additions and 0 deletions

View File

@ -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<Widget> 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();
}
}

View File

@ -0,0 +1,26 @@
<div class="mdl-form__row mdl-cell mdl-cell--6-col mdl-cell--8-col-tablet %visibility_class%">
<span %iconstyle% class="mdl-form__icon">
%icon_snippet%
</span>
<span %labelstyle% class="mdl-form__label">
%label%
</span>
<div
class="mdl-form__control mdl-form__input mdl-textfield mdl-js-textfield"
data-control-type="input"
data-item="%item%"
data-widget-id="%widget_id%"
data-item-type=%data_type%
for="oh-input-%item%"
>
<input
%valuestyle% type="text"
id="oh-input-%item%"
class="mdl-textfield__input"
value="%data_state%"
/>
<label for="oh-input-%item%" class="mdl-textfield__label">
%undef_state%
</label>
</div>
</div>

View File

@ -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;

View File

@ -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));
}

View File

@ -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",