[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
parent
4edde23af8
commit
4e74d056d0
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue