iobroker.schedule-switcher
Version:
Switch states over scheduler
499 lines (463 loc) • 21.9 kB
JavaScript
(async () => {
class OneTimeTrigger extends HTMLElement {
constructor() {
super();
this.sr = this.createShadowRoot();
this.errors = [];
this.triggerErrors = [];
this.triedSaving = false;
this.milliseconds = -1;
this.setDate = null;
this.interval = null;
this.noInput = true;
this.intervalTime = 500;
this.checktime = new Date();
}
static get observedAttributes() {
return ["trigger", "action"];
}
connectedCallback() {
this.triedSaving = false;
this.sr.querySelector(".button.delete").addEventListener("click", this.onDeleteClick.bind(this));
this.sr.querySelector(".button.cancel").addEventListener("click", this.onCancelClick.bind(this));
this.sr.querySelector(".button.save").addEventListener("click", this.onSaveClick.bind(this));
this.sr.querySelector(".button.add").addEventListener("click", this.onAddConditionClick.bind(this));
this.sr.querySelector(".button.edit").addEventListener("click", this.onEditClick.bind(this));
this.sr.querySelector("input.hours").addEventListener("input", this.onTimeInput.bind(this));
this.sr.querySelector("input.minutes").addEventListener("input", this.onTimeInput.bind(this));
this.sr.querySelector("input.seconds").addEventListener("input", this.onTimeInput.bind(this));
this.sr.querySelector("input.datetime").addEventListener("input", this.onDateTimeInput.bind(this));
this.sr.querySelector("#radio-date").addEventListener("input", this.onDateInput.bind(this));
this.sr.querySelector("#radio-time").addEventListener("input", this.onDateInput.bind(this));
if (this.trigger) {
const checktime = new Date(Date.parse(this.trigger.date));
if (this.checktime < new Date()) {
console.log("Expired - Delete trigger!");
this.interval && clearTimeout(this.interval);
this.onDeleteClick();
}
}
}
updateTimeUntilTrigger() {
if (this.trigger) {
this.interval = null;
this.sr.querySelector(".time").textContent = this.millisecondsToHuman(
new Date(this.trigger.date) - new Date(),
);
if (this.checktime < new Date()) {
return;
}
this.interval = setTimeout(() => this.updateTimeUntilTrigger(), this.intervalTime);
}
}
targetTimeTrigger() {
if (this.trigger && !this.trigger.timedate) {
this.sr.querySelector(".targettime").textContent = this.targetTime();
}
}
onEditClick() {
this.timedate = this.trigger.timedate;
this.sr.querySelector(".container.edit").style.display = null;
this.sr.querySelector(".container.view").style.display = "none";
this.setViews();
if (this.trigger.timedate) {
const ms = new Date(this.trigger.date) - new Date();
this.sr.querySelector("input.seconds").value = Math.floor((ms / 1000) % 60);
this.sr.querySelector("input.minutes").value = Math.floor((ms / 1000 / 60) % 60);
this.sr.querySelector("input.hours").value = Math.floor((ms / 1000 / 3600) % 24);
this.sr.querySelector(`#radio-time`).checked = true;
this.onTimeInput();
} else {
this.sr.querySelector(`#radio-date`).checked = true;
this.setDateTime();
}
}
attributeChangedCallback(attr) {
if (attr === "action") {
this.onActionChange();
} else if (attr === "trigger") {
this.onTriggerChange();
}
}
get timedate() {
const attrValue = this.getAttribute("timedate");
return attrValue === "true";
}
set timedate(val) {
this.setAttribute("timedate", val);
}
get trigger() {
if (this.hasAttribute("trigger")) {
return JSON.parse(this.getAttribute("trigger"));
}
return null;
}
get action() {
return JSON.parse(this.getAttribute("action"));
}
onCancelClick() {
if (this.trigger && this.trigger.id != null) {
this.sr.querySelector(".container.edit").style.display = "none";
this.sr.querySelector(".container.view").style.display = null;
return;
}
this.setAttribute("trigger", null);
this.sr.dispatchEvent(
new CustomEvent("cancel-one-time-trigger-creation", {
detail: {},
composed: true,
}),
);
}
onDeleteClick() {
this.sr.dispatchEvent(
new CustomEvent("delete-one-time-trigger", {
detail: {},
composed: true,
}),
);
this.sr.dispatchEvent(
new CustomEvent("delete", {
detail: { id: this.trigger.id },
composed: true,
}),
);
}
onDeleteConditionClick() {
if (this.action.type === "ConditionAction") {
this.setAttribute("action", JSON.stringify(this.action.action));
}
}
onAddConditionClick() {
const conditionAction = vis.binds["schedule-switcher"].addConditionToAction(
this.action,
this.getAttribute("widgetid"),
);
if (conditionAction) {
this.setAttribute("action", JSON.stringify(conditionAction));
}
}
onDateInput() {
this.timedate = this.sr.querySelector("#radio-time").checked;
this.setViews();
this.errors = [];
this.triggerErrors = [];
this.noInput = true;
if (this.timedate) {
this.setDate = null;
} else {
this.setDateTime();
}
}
setViews() {
if (this.timedate) {
this.sr.querySelector(".container.edit .trigger").style.display = null;
this.sr.querySelector("#trigger-header").style.display = null;
this.sr.querySelector(".container.edit .trigger-date").style.display = "none";
this.sr.querySelector("#trigger-date-header").style.display = "none";
} else {
this.sr.querySelector(".container.edit .trigger").style.display = "none";
this.sr.querySelector("#trigger-header").style.display = "none";
this.sr.querySelector(".container.edit .trigger-date").style.display = null;
this.sr.querySelector("#trigger-date-header").style.display = null;
}
}
setDateTime() {
const newToday = new Date();
let triggerDate;
if (this.trigger && this.trigger.date) {
triggerDate = new Date(this.trigger.date);
}
const today = this.trigger && !this.trigger.timedate ? triggerDate : newToday;
this.sr.querySelector("#datetime").value = this.formatDate(today, 0);
this.sr.querySelector("#datetime").min = this.formatDate(newToday, 0);
this.sr.querySelector("#datetime").max = this.formatDate(newToday, 1);
}
formatDate(date, endYear) {
return (
date.getFullYear() +
endYear +
"-" +
(date.getMonth() + 1).toString().padStart(2, "0") +
"-" +
date.getDate() +
"T" +
date.getHours().toString().padStart(2, "0") +
":" +
date.getMinutes().toString().padStart(2, "0")
);
}
onSaveClick() {
this.updateValidationErrors();
if (this.errors.length === 0) {
let saveDate;
if (!this.timedate && this.setDate) {
saveDate = new Date(this.setDate).toISOString();
} else {
saveDate = new Date(new Date().getTime() + this.milliseconds).toISOString();
}
if (this.trigger && this.trigger.id != null) {
this.sr.querySelector(".container.edit").style.display = "none";
this.sr.querySelector(".container.view").style.display = null;
this.sr.dispatchEvent(
new CustomEvent("update", {
detail: {
trigger: {
id: this.trigger.id,
type: "OneTimeTrigger",
timedate: this.timedate,
date: saveDate,
action: JSON.parse(this.getActionElement(true).getAttribute("data")),
},
},
composed: true,
}),
);
return;
}
this.sr.dispatchEvent(
new CustomEvent("create", {
detail: {
trigger: {
type: "OneTimeTrigger",
timedate: this.timedate,
date: saveDate,
action: JSON.parse(this.getActionElement(true).getAttribute("data")),
},
},
composed: true,
}),
);
this.sr.dispatchEvent(
new CustomEvent("cancel-one-time-trigger-creation", {
detail: {},
composed: true,
}),
);
} else {
if (!this.triedSaving) {
this.getActionElement(true).addEventListener("errors", this.updateValidationErrors.bind(this));
}
}
this.triedSaving = true;
}
updateValidationErrors() {
const errorsAction = JSON.parse(this.getActionElement(true).getAttribute("errors"));
let errors = [];
if (this.noInput) {
errors.push(vis.binds["schedule-switcher"].translate("errorTime"));
}
if (this.triggerErrors) {
errors = errors.concat(this.triggerErrors);
}
if (errorsAction) {
errors = errors.concat(errorsAction);
}
this.errors = errors;
this.sr.querySelector(".validation-errors-container").style.display = errors.length === 0 ? "none" : null;
const validationErrorsList = this.sr.querySelector("#validation-errors");
while (validationErrorsList.firstChild) {
validationErrorsList.removeChild(validationErrorsList.firstChild);
}
this.errors.forEach((e) => {
const li = document.createElement("li");
li.textContent = e;
validationErrorsList.appendChild(li);
});
}
onDateTimeInput() {
const datetime = this.sr.querySelector("input.datetime").value;
this.setDate = datetime;
const check_date = new Date(this.setDate);
const min_date = new Date(this.sr.querySelector("input.datetime").min);
const max_date = new Date(this.sr.querySelector("input.datetime").max);
this.noInput = false;
const errors = [];
if (isNaN(check_date.getTime())) {
errors.push(vis.binds["schedule-switcher"].translate("errorInvalid"));
}
if (check_date <= min_date) {
errors.push(vis.binds["schedule-switcher"].translate("errorLess"));
}
if (check_date >= max_date) {
errors.push(vis.binds["schedule-switcher"].translate("errorGreater"));
}
this.triggerErrors = errors;
if (this.triedSaving) {
this.updateValidationErrors();
}
}
onTimeInput() {
this.noInput = false;
const hours = Number.parseInt(this.sr.querySelector("input.hours").value, 10);
const minutes = Number.parseInt(this.sr.querySelector("input.minutes").value, 10);
const seconds = Number.parseInt(this.sr.querySelector("input.seconds").value, 10);
const errors = [];
if (Number.isNaN(hours) || hours < 0) {
errors.push(vis.binds["schedule-switcher"].translate("errorHour"));
}
if (Number.isNaN(minutes) || minutes < 0 || minutes > 59) {
errors.push(vis.binds["schedule-switcher"].translate("errorMinutes"));
}
if (Number.isNaN(seconds) || seconds < 0 || seconds > 59) {
errors.push(vis.binds["schedule-switcher"].translate("errorSekundes"));
}
if (seconds === 0 && minutes === 0 && hours === 0) {
this.noInput = true;
}
if (errors.length === 0) {
this.milliseconds = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
}
this.triggerErrors = errors;
if (this.triedSaving) {
this.updateValidationErrors();
}
}
createShadowRoot() {
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<link rel="stylesheet" href="widgets/schedule-switcher/css/OneTimeTrigger.css"/>
<link rel="stylesheet" href="widgets/schedule-switcher/css/material-radio-button.css"/>
<div class="container view" style="display: none">
<div class="header">
<div class="action"></div>
<img class="date icon"/>
<img class="button edit" src="widgets/schedule-switcher/img/edit-24px.svg" width="28px"
height="28px" title="${vis.binds["schedule-switcher"].translate("editTrigger")}"/>
<img class="button delete" src="widgets/schedule-switcher/img/delete-24px.svg" width="28px"
height="28px" title="${vis.binds["schedule-switcher"].translate("removeTrigger")}"/>
</div>
<div class="header">
<div class="trigger">
<div class="time"></div>
<div class="targettime"></div>
</div>
</div>
</div>
<div class="container edit">
<div class="header">
<img class="button save" src="widgets/schedule-switcher/img/save-24px.svg" width="28px"
height="28px" title="${vis.binds["schedule-switcher"].translate("saveChanges")}"/>
<img class="button cancel" src="widgets/schedule-switcher/img/cancel-24px.svg" width="28px"
height="28px" title="${vis.binds["schedule-switcher"].translate("cancelEdit")}"/>
</div>
<div class="validation-errors-container" style="display: none;">
<ul id="validation-errors"></ul>
</div>
<div>${vis.binds["schedule-switcher"].translate("switchedValue")}</div>
<div class="action"></div>
<div class="condition">
<div>${vis.binds["schedule-switcher"].translate("condition")}</div>
<img class="button add" src="widgets/schedule-switcher/img/add-24px.svg" width="28px"
height="28px" title="${vis.binds["schedule-switcher"].translate("addCondition")}"/>
</div>
<div class="md-radio md-radio-inline">
<input id="radio-time" type="radio" name="switched-value-date"/>
<label for="radio-time">${vis.binds["schedule-switcher"].translate("withTime")}</label>
</div>
<div class="md-radio md-radio-inline">
<input id="radio-date" type="radio" name="switched-value-date"/>
<label for="radio-date">${vis.binds["schedule-switcher"].translate("withDate")}</label>
</div>
<div id="trigger-header">${vis.binds["schedule-switcher"].translate("oneTimeTriggerInfo")}</div>
<div class="trigger">
<input type="number" class="hours" min="0" max="23" step="1" placeholder="h" required value="0">
<span>:</span>
<input type="number" class="minutes" min="0" max="59" step="1" placeholder="mm" required value="0">
<span>:</span>
<input type="number" class="seconds" min="0" max="59" step="1" placeholder="ss" required value="0">
</div>
<div id="trigger-date-header">${vis.binds["schedule-switcher"].translate("selectTimeDate")}</div>
<div class="trigger-date">
<input class="datetime" type="datetime-local" name="datetime" id="datetime" required />
</div>
</div>
`;
return shadowRoot;
}
getActionElement(edit) {
const newAction = this.action;
const elementName = vis.binds["schedule-switcher"].getElementNameForActionType(newAction.type);
return this.sr.querySelector(`.container.${edit ? "edit" : "view"} .action ${elementName}`);
}
onTriggerChange() {
const newTrigger = this.trigger;
if (newTrigger) {
if (!this.trigger.timedate) {
this.intervalTime = 59500;
}
this.checktime = new Date(Date.parse(this.trigger.date));
this.updateTimeUntilTrigger();
this.targetTimeTrigger();
this.sr.querySelector(".container.edit").style.display = "none";
this.sr.querySelector(".container.view").style.display = null;
}
}
onActionChange() {
const newAction = this.action;
const elementName = vis.binds["schedule-switcher"].getElementNameForActionType(newAction.type);
const viewAction = this.sr.querySelector(".container.view .action");
if (viewAction.firstChild) {
viewAction.removeChild(viewAction.firstChild);
}
const actionView = document.createElement(elementName);
actionView.setAttribute("widgetid", this.getAttribute("widgetid"));
actionView.setAttribute("edit", "false");
viewAction.appendChild(actionView);
const editAction = this.sr.querySelector(".container.edit .action");
if (editAction.firstChild) {
editAction.removeChild(editAction.firstChild);
}
const iconElement = this.sr.querySelector(".view .date.icon");
iconElement.src = `widgets/schedule-switcher/img/onetime-24px.svg`;
iconElement.alt = vis.binds["schedule-switcher"].translate("oneTimeTriggerInfo");
iconElement.title = vis.binds["schedule-switcher"].translate("oneTimeTriggerInfo");
const actionEdit = document.createElement(elementName);
actionEdit.setAttribute("widgetid", this.getAttribute("widgetid"));
actionEdit.setAttribute("edit", "true");
actionEdit.addEventListener("delete-condition", this.onDeleteConditionClick.bind(this));
editAction.appendChild(actionEdit);
actionView.setAttribute("data", JSON.stringify(newAction));
actionEdit.setAttribute("data", JSON.stringify(newAction));
this.sr.querySelector(".condition").style.display = newAction.type === "ConditionAction" ? "none" : null;
this.timedate = "true";
this.sr.querySelector(`#radio-time`).checked = true;
this.sr.querySelector(".container.edit .trigger-date").style.display = "none";
this.sr.querySelector("#trigger-date-header").style.display = "none";
}
targetTime() {
if (!this.trigger.timedate) {
const n = new Date(this.trigger.date);
return `${vis.binds["schedule-switcher"].translate("targetTime")} - ${this.dateView(n)}`;
}
}
millisecondsToHuman(ms) {
if (this.trigger && !this.trigger.timedate) {
const d = new Date();
return `${vis.binds["schedule-switcher"].translate("actualTime")} - ${this.dateView(d)}`;
}
const seconds = Math.floor((ms / 1000) % 60);
const minutes = Math.floor((ms / 1000 / 60) % 60);
const hours = Math.floor((ms / 1000 / 3600) % 24);
const humanized = [hours, minutes, seconds]
.map((v) => (v < 0 ? 0 : v))
.map((v) => v.toString().padStart(2, "0"))
.join(":");
return `T - ${humanized}`;
}
dateView(d) {
return (
d.getFullYear() +
"-" +
("0" + (d.getMonth() + 1)).slice(-2) +
"-" +
("0" + d.getDate()).slice(-2) +
" " +
("0" + d.getHours()).slice(-2) +
":" +
("0" + d.getMinutes()).slice(-2)
);
}
}
customElements.define("app-one-time-trigger-schedule", OneTimeTrigger);
})();