UNPKG

@openhab-ui/setup-and-maintenance

Version:

Configuration and maintenance interface for openHAB

1,828 lines (1,533 loc) 68.9 kB
import { createNotification } from './app.js'; import { Vue } from './vue.js'; import { store } from '../js/app.js'; class Component { constructor(name) { if (this.constructor === Component) throw new TypeError('Can not construct abstract class.'); this.name = name; this.data = {}; } worker() { } } class Control { constructor(key) { if (this.constructor === Control) throw new TypeError('Can not construct abstract class'); if (!key) throw new Error('The key parameter is missing in super() of Control '); this.key = key; this.data = {}; this.parent = null; } getNode() { if (this.parent === null) throw new Error("Control isn't added to Node/Input"); return this.parent instanceof Node ? this.parent : this.parent.node; } getData(key) { return this.getNode().data[key]; } putData(key, data) { this.getNode().data[key] = data; return this;//ADDED } } class Connection { constructor(output, input) { this.output = output; this.input = input; this.data = {}; this.input.addConnection(this); } remove() { this.input.removeConnection(this); this.output.removeConnection(this); } } class IO { constructor(key, name, socket, multiConns) { this.node = null; this.multipleConnections = multiConns; this.connections = []; this.key = key; this.name = name; this.socket = socket; } removeConnection(connection) { this.connections.splice(this.connections.indexOf(connection), 1); } removeConnections() { this.connections.map(connection => this.removeConnection(connection)); } } class Socket { constructor(name, data = {}) { this.name = name; this.data = data; this.compatible = []; } combineWith(socketName) { this.compatible.push(socketName); } compatibleWith(socket) { return this.name == socket.name || socket.compatible.includes(this.name); } } class Input extends IO { constructor(key, title, socket, multiConns = false) { super(key, title, socket, multiConns); this.control = null; } hasConnection() { return this.connections.length > 0; } addConnection(connection) { if (!this.multipleConnections && this.hasConnection()) throw new Error('Multiple connections not allowed'); this.connections.push(connection); } addControl(control) { this.control = control; control.parent = this; } showControl() { return !this.hasConnection() && this.control !== null; } } class Output extends IO { constructor(key, title, socket, multiConns = true) { super(key, title, socket, multiConns); } hasConnection() { return this.connections.length > 0; } connectTo(input) { if (!this.socket.compatibleWith(input.socket)) throw new Error('Sockets not compatible'); if (!input.multipleConnections && input.hasConnection()) throw new Error('Input already has one connection'); if (!this.multipleConnections && this.hasConnection()) throw new Error('Output already has one connection'); var connection = new Connection(this, input); this.connections.push(connection); return connection; } connectedTo(input) { return this.connections.some((item) => { return item.input === input; }); } } class Node { constructor(name) { this.name = name; this.id = Node.incrementId(); this.position = [0.0, 0.0]; this.inputs = new Map(); this.outputs = new Map(); this.controls = new Map(); this.data = {}; this.meta = {}; } addControl(control) { control.parent = this; console.log("ADD CONTROL", control); this.controls.set(control.key, control); return this; } removeControl(control) { control.parent = null; this.controls.delete(control.key); } addInput(input) { if (input.node !== null) throw new Error('Input has already been added to the node'); input.node = this; this.inputs.set(input.key, input); return this; } removeInput(input) { input.removeConnections(); input.node = null; this.inputs.delete(input.key); } addOutput(output) { if (output.node !== null) throw new Error('Output has already been added to the node'); output.node = this; this.outputs.set(output.key, output); return this; } removeOutput(output) { output.removeConnections(); output.node = null; this.outputs.delete(output.key); } getConnections() { const ios = [...this.inputs.values(), ...this.outputs.values()]; const connections = ios.reduce((arr, io) => { return [...arr, ...io.connections]; }, []); return connections; } update() { } static incrementId() { if (!this.latestId) this.latestId = 1; else this.latestId++; return this.latestId } } class Component$1 extends Component { constructor(name) { super(name); if (this.constructor === Component$1) throw new TypeError('Can not construct abstract class.'); this.editor = null; this.data = {}; } async builder() { } async build(node) { await this.builder(node); return node; } async createNode(data = {}) { const node = new Node(this.name); node.data = data; await this.build(node); return node; } } class Events { constructor(handlers) { this.handlers = { warn: [console.warn], error: [console.error], ...handlers }; } } class Emitter { constructor(events) { this.events = events instanceof Emitter ? events.events : events.handlers; } on(names, handler) { names.split(' ').forEach(name => { if (!this.events[name]) throw new Error(`The event ${name} does not exist`); this.events[name].push(handler); }); return this; } trigger(name, params) { if (!(name in this.events)) throw new Error(`The event ${name} cannot be triggered`); return this.events[name].reduce((r, e) => { return (e(params) !== false) && r }, true); // return false if at least one event is false } bind(name) { if (this.events[name]) throw new Error(`The event ${name} is already bound`); this.events[name] = []; } exist(name) { return Array.isArray(this.events[name]); } } class Context extends Emitter { constructor(id, events) { super(events); if (!/^[\w-]{3,}@[0-9]+\.[0-9]+\.[0-9]+$/.test(id)) throw new Error('ID should be valid to name@0.1.0 format'); this.id = id; this.plugins = new Map(); } use(plugin, options = {}) { if (plugin.name && this.plugins.has(plugin.name)) throw new Error(`Plugin ${plugin.name} already in use`) plugin.install(this, options); this.plugins.set(plugin.name, options); } } class EditorEvents extends Events { constructor() { super({ nodecreate: [], nodecreated: [], noderemove: [], noderemoved: [], connectioncreate: [], connectioncreated: [], connectionremove: [], connectionremoved: [], translatenode: [], nodetranslate: [], nodetranslated: [], nodedraged: [], selectnode: [], nodeselect: [], nodeselected: [], rendernode: [], rendersocket: [], rendercontrol: [], renderconnection: [], updateconnection: [], componentregister: [], cleared: [], keydown: [], keyup: [], translate: [], translated: [], zoom: [], zoomed: [], click: [], mousemove: [], contextmenu: [], import: [], afterImport: [], export: [], process: [], showeditor: [] }); } } class Area extends Emitter { constructor(container, emitter) { super(emitter); const el = this.el = document.createElement('div'); this.container = container; this.transform = { k: 1, x: 0, y: 0 }; this.mouse = { x: 0, y: 0 }; el.style.transformOrigin = '0 0'; this._startPosition = null; // this._zoom = new Zoom(container, el, 0.1, this.onZoom.bind(this)); //this._drag = new Drag(container, this.onTranslate.bind(this), this.onStart.bind(this)); this.container.addEventListener('mousemove', this.mousemove.bind(this)); this.update(); } dispose() { // this._drag.dispose(); } update() { const t = this.transform; this.el.style.transform = `translate(${t.x}px, ${t.y}px) scale(${t.k})`; } mousemove(e) { const rect = this.el.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const k = this.transform.k; this.mouse = { x: x / k, y: y / k }; this.trigger('mousemove', { ...this.mouse }); } onStart() { this._startPosition = { ...this.transform }; } onTranslate(dx, dy) { this.translate(this._startPosition.x + dx, this._startPosition.y + dy); } onZoom(delta, ox, oy, source) { this.zoom(this.transform.k * (1 + delta), ox, oy, source); this.update(); } translate(x, y) { const params = { transform: this.transform, x, y }; if (!this.trigger('translate', params)) return; this.transform.x = params.x; this.transform.y = params.y; this.update(); this.trigger('translated'); } zoom(zoom, ox = 0, oy = 0, source) { const k = this.transform.k; const params = { transform: this.transform, zoom, source }; if (!this.trigger('zoom', params)) return; const d = (k - params.zoom) / ((k - zoom) || 1); this.transform.k = params.zoom || 1; this.transform.x += ox * d; this.transform.y += oy * d; this.update(); this.trigger('zoomed', { source }); } appendChild(el) { this.el.appendChild(el); } removeChild(el) { this.el.removeChild(el); } } class Control$1 extends Emitter { constructor(el, control, emitter) { super(emitter); this.trigger('rendercontrol', { el, control }); } } class Socket$1 extends Emitter { constructor(el, type, io, node, emitter) { super(emitter); this.el = el; this.type = type; this.io = io; this.node = node; this.trigger('rendersocket', { el, [type]: this.io, socket: io.socket }); } getPosition({ position }) { const el = this.el; return [ position[0] + el.offsetLeft + el.offsetWidth / 2, position[1] + el.offsetTop + el.offsetHeight / 2 ] } } class Node$1 extends Emitter { constructor(node, component, emitter) { super(emitter); this.node = node; this.component = component; this.sockets = new Map(); this.controls = new Map(); this.el = document.createElement('div'); this.el.classList.add("nodewrapper"); this.el.addEventListener('contextmenu', e => this.trigger('contextmenu', { e, node: this.node })); this._startPosition = null; // this._drag = new Drag(this.el, this.onTranslate.bind(this), this.onSelect.bind(this), () => { // this.trigger('nodedraged', node); // }); this.trigger('rendernode', { el: this.el, node, component: component.data, bindSocket: this.bindSocket.bind(this), bindControl: this.bindControl.bind(this) }); this.update(); } dispose() { // this._drag.dispose(); } bindSocket(el, type, io) { this.sockets.set(io, new Socket$1(el, type, io, this.node, this)); } bindControl(el, control) { this.controls.set(control, new Control$1(el, control, this)); } getSocketPosition(io) { return this.sockets.get(io).getPosition(this.node); } onSelect(e) { this.onStart(); this.trigger('selectnode', { node: this.node, accumulate: e.ctrlKey }); } onStart() { this._startPosition = [...this.node.position]; } onTranslate(dx, dy) { this.trigger('translatenode', { node: this.node, dx, dy }); } onDrag(dx, dy) { const x = this._startPosition[0] + dx; const y = this._startPosition[1] + dy; this.translate(x, y); } translate(x, y) { const node = this.node; const params = { node, x, y }; if (!this.trigger('nodetranslate', params)) return; const prev = [...node.position]; node.position[0] = params.x; node.position[1] = params.y; this.update(); this.trigger('nodetranslated', { node, prev }); } update() { this.el.style.transform = `translate(${this.node.position[0]}px, ${this.node.position[1]}px)`; } } class Connection$1 extends Emitter { constructor(connection, inputNode, outputNode, emitter) { super(emitter); this.connection = connection; this.inputNode = inputNode; this.outputNode = outputNode; this.el = document.createElement('div'); this.el.style.position = 'absolute'; this.el.style.zIndex = '-1'; this.trigger('renderconnection', { el: this.el, connection: this.connection, points: this.getPoints() }); } getPoints() { const [x1, y1] = this.outputNode.getSocketPosition(this.connection.output); const [x2, y2] = this.inputNode.getSocketPosition(this.connection.input); return [x1, y1, x2, y2]; } update() { this.trigger('updateconnection', { el: this.el, connection: this.connection, points: this.getPoints() }); } } class EditorView extends Emitter { constructor(container, components, emitter) { super(emitter); this.container = container; this.components = components; this.nodes = new Map(); this.connections = new Map(); this.container.addEventListener('click', this.click.bind(this)); this.container.addEventListener('contextmenu', e => this.trigger('contextmenu', { e, view: this })); // this.boundResize = this.resize.bind(this); // window.addEventListener('resize', this.boundResize); //this.on('nodetranslated', this.updateConnections.bind(this)); this.area = new Area(container, this); this.container.appendChild(this.area.el); } dispose() { this.nodes.forEach(e => e.dispose()); this.area.dispose(); // window.removeEventListener('resize', this.boundResize); } addNode(node) { const nodeView = new Node$1(node, this.components.get(node.name), this); this.nodes.set(node, nodeView); this.area.appendChild(nodeView.el); return nodeView; } removeNode(node) { const nodeView = this.nodes.get(node); nodeView.dispose(); this.nodes.delete(node); this.area.removeChild(nodeView.el); } addConnection(connection) { const viewInput = this.nodes.get(connection.input.node); const viewOutput = this.nodes.get(connection.output.node); const connView = new Connection$1(connection, viewInput, viewOutput, this); this.connections.set(connection, connView); this.area.appendChild(connView.el); } removeConnection(connection) { const connView = this.connections.get(connection); this.connections.delete(connection); this.area.removeChild(connView.el); } updateConnections({ node }) { node.getConnections().map(conn => { this.connections.get(conn).update(); }); } click(e) { const container = this.container; if (container !== e.target) return; if (!this.trigger('click', { e, container })) return; } } class Selected { constructor() { this.list = []; } add(item, accumulate = false) { if (!accumulate) this.list = [item]; else if (!this.contains(item)) this.list.push(item); } clear() { this.list = []; } remove(item) { this.list.splice(this.list.indexOf(item), 1); } contains(item) { return this.list.indexOf(item) !== -1; } each(callback) { this.list.forEach(callback); } } class NodeEditor extends Context { constructor(id, container) { super(id, new EditorEvents()); this.nodes = []; this.components = new Map(); this.silent = false; this.selected = new Selected(); this.view = new EditorView(container, this.components, this); this.boundTriggerKeydown = e => this.trigger('keydown', e); this.boundTriggerKeyup = e => this.trigger('keyup', e); window.addEventListener('keydown', this.boundTriggerKeydown); window.addEventListener('keyup', this.boundTriggerKeyup); this.on('selectnode', ({ node, accumulate }) => this.selectNode(node, accumulate)); this.on('nodeselected', () => this.selected.each(n => this.view.nodes.get(n).onStart())); this.on('translatenode', ({ dx, dy }) => this.selected.each(n => this.view.nodes.get(n).onDrag(dx, dy))); } dispose() { this.view.dispose(); window.removeEventListener('keydown', this.boundTriggerKeydown); window.removeEventListener('keyup', this.boundTriggerKeyup); } addNode(node) { if (!this.trigger('nodecreate', node)) return null; this.nodes.push(node); const nodeView = this.view.addNode(node); this.trigger('nodecreated', node); return nodeView; } removeNode(node) { if (!this.trigger('noderemove', node)) return; node.getConnections().forEach(c => this.removeConnection(c)); this.nodes.splice(this.nodes.indexOf(node), 1); this.view.removeNode(node); this.trigger('noderemoved', node); } connect(output, input, data = {}) { if (!this.trigger('connectioncreate', { output, input })) return; try { const connection = output.connectTo(input); connection.data = data; this.view.addConnection(connection); this.trigger('connectioncreated', connection); } catch (e) { this.trigger('warn', e); } } removeConnection(connection) { if (!this.trigger('connectionremove', connection)) return; this.view.removeConnection(connection); connection.remove(); this.trigger('connectionremoved', connection); } selectNode(node, accumulate = false) { if (this.nodes.indexOf(node) === -1) throw new Error('Node not exist in list'); if (!this.trigger('nodeselect', node)) return; this.selected.add(node, accumulate); this.trigger('nodeselected', node); } getComponent(name) { const component = this.components.get(name); if (!component) throw `Component ${name} not found`; return component; } register(component) { component.editor = this; this.components.set(component.name, component); this.trigger('componentregister', component); } clear() { [...this.nodes].map(node => this.removeNode(node)); } beforeImport(data, clear = false) { this.silent = true; if (clear) this.clear(); if (data) this.trigger('import', data); } afterImport() { this.silent = false; this.trigger('afterImport'); } isSilent() { return this.silent; } } /** * A vue mixin, that allows a dropdown or multiselect element * (or any dom element that takes an "options" property) to be populated * with OH Model data. The data is fetched on-demand from the Model Store * as soon as the element has been rendered by vue. * * This is realized by the following vue directive called "v-dynamicload", * which as soon as the element binding happens, calls "performLoad". * * The data is fetched once and not updated if the underlying model changes. * The data is cached, as long the the parent root vue is existing. * * If you want the data to be refreshed when another vue element changed, you can * use a reactive variable instead of a static filter object as value. * * @example Example for a dropdown box that shows all available bindings, using the model table "bindings". * <ui-dropdown viewkey="name" desckey="description" valuekey="id" v-dynamicload:bindings="" ></ui-dropdown> * * @example You can filter items of a table by using the directives value. * The value is an array of filter objects with a "name" and a regex "value". * <ui-dropdown viewkey="name" desckey="description" valuekey="id" v-dynamicload:bindings="[{ name: 'id', value: '^mqtt:' }]" ></ui-dropdown> * * @example Use a reactive variable "bindingids". The directive will be executed again * when the variable changes. * <ui-dropdown viewkey="name" desckey="description" valuekey="id" v-dynamicload:bindings="bindingids" ></ui-dropdown> */ const DynamicLoadMixin = { directives: { dynamicload: { bind: (el, binding) => performLoad(el, binding), // Avoid to re-populate the dropdown/multiselect with same values by diffing old and old new filter array update: (el, binding) => { const newIsArray = Array.isArray(binding.value); const oldIsArray = Array.isArray(binding.oldValue); if (!newIsArray && !oldIsArray) return; // Both no valid values: exit if (newIsArray != oldIsArray) { // One is an array the other not: Perform an option recomputation performLoad(el, binding); return; } // Compute a string out of the filters and compare to saved filters string const filterString = binding.value.reduce((acc, filter) => acc += filter.name + filter.value, ""); if (el.dataset.dynload_filterString != filterString) { performLoad(el, binding); } } } }, }; /** * This is the cache for all "v-dynamicload"s of one root vue instance. * There is no way to force a cache refresh, so use this mixin only for * static cases like selecting an Item for a Rule action or similar. */ const dynamicallyFetchedSharedData = {}; /** * Load a table from the backend store (like "rules","things" and so on) * and assign the asynchronously gathered data to the target dom element. * The target element is assumed to be a dropdown or multiselect element. * * @param {Element} el A dom element * @param {Object} binding A vue directive binding object */ async function performLoad(el, binding) { el.dynamicLoadBind = binding; // The user wants the element to be deliberately empty if (binding.value === null) { el.options = []; return; } // First try to get a cached value let tabledata = dynamicallyFetchedSharedData[binding.arg]; if (!tabledata) { const ischannel = binding.arg == "channels"; if (!store.connected) { console.warn(`Dynamic load of ${binding.arg} failed. Not connected`); return; } // A lot of dom elements can ask for the same dynamically loaded data. We therefore store // the fetch promise in the cache object. Any consecutive load requests will receive the promise // (as long as the fetch is not resolved) instead of performing a parallel second, third.. fetch. const fetchPromise = store.get(ischannel ? "things" : binding.arg, null, { force: true }); dynamicallyFetchedSharedData[binding.arg] = fetchPromise; tabledata = await fetchPromise.catch(e => console.warn(`Dynamic load of ${binding.arg} failed`, e)); if (!tabledata) return; if (ischannel) { let channels = []; for (let thing of tabledata) { channels = channels.concat(thing.channels); } tabledata = channels; } // Assign value to the cache dynamicallyFetchedSharedData[binding.arg] = tabledata; } // If cached data is a promise, resolve it now if (tabledata.then) tabledata = await tabledata; // Apply filters. This is for example used to filter "channels" by their "kind" (STATE, TRIGGER). // A filter looks like this: {"value": "TRIGGER","name": "kind"} let filters = Array.isArray(binding.value) ? binding.value : []; let filterString = ""; for (let filter of filters) { const regex = new RegExp(filter.value); tabledata = tabledata.filter(item => item[filter.name].match(regex)); filterString += filter.name + filter.value; } // Assign value to the target dom el.options = tabledata; el.dataset.dynload_filterString = filterString; } // var script = { // If condensed is true: Render an "edit" button instead of inlining the component. // Useful for multi-line inputs and map widgets. props: { param: Object, value: [String, Number, Boolean, Object], condensed: { type: Boolean, default: false }, desc: { type: Boolean, default: true } }, computed: { canremove() { this.allowremove && this.param.defaultValue; } }, mixins: [DynamicLoadMixin], methods: { get() { return this.value || this.param.defaultValue; }, remove() { this.$emit("remove", this.param); }, set(event) { this.$emit("input", event.target.value); }, mapChanged(event) { this.$refs.mapcoordinates.value = event.target.value[0] + "," + event.target.value[1]; this.$emit("input", this.$refs.mapcoordinates.value); } } }; function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { if (typeof shadowMode !== 'boolean') { createInjectorSSR = createInjector; createInjector = shadowMode; shadowMode = false; } // Vue.extend constructor export interop. const options = typeof script === 'function' ? script.options : script; // render functions if (template && template.render) { options.render = template.render; options.staticRenderFns = template.staticRenderFns; options._compiled = true; // functional template if (isFunctionalTemplate) { options.functional = true; } } // scopedId if (scopeId) { options._scopeId = scopeId; } let hook; if (moduleIdentifier) { // server build hook = function (context) { // 2.3 injection context = context || // cached call (this.$vnode && this.$vnode.ssrContext) || // stateful (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional // 2.2 with runInNewContext: true if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__; } // inject component styles if (style) { style.call(this, createInjectorSSR(context)); } // register component module identifier for async chunk inference if (context && context._registeredComponents) { context._registeredComponents.add(moduleIdentifier); } }; // used by ssr in case component is cached and beforeCreate // never gets called options._ssrRegister = hook; } else if (style) { hook = shadowMode ? function () { style.call(this, createInjectorShadow(this.$root.$options.shadowRoot)); } : function (context) { style.call(this, createInjector(context)); }; } if (hook) { if (options.functional) { // register for functional component in vue file const originalRender = options.render; options.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context); }; } else { // inject component registration as beforeCreate hook const existing = options.beforeCreate; options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } return script; } /* script */ const __vue_script__ = script; /* template */ var __vue_render__ = function() { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c( "div", { staticClass: "element", class: { isboolean: _vm.param.type == "BOOLEAN" } }, [ _vm.param.type != "BOOLEAN" ? _c("label", { attrs: { title: _vm.param.description } }, [ _vm._v(_vm._s(_vm.param.label)) ]) : _vm._e(), _vm._v(" "), _c( "button", { staticClass: "btn btn-outline-danger btn-sm resetdefault", attrs: { disabled: !_vm.value, title: "Reset to default" }, on: { click: function($event) { $event.preventDefault(); return _vm.remove() } } }, [_c("i", { staticClass: "fas fa-undo" })] ), _vm._v(" "), _vm.param.type == "BOOLEAN" ? _c("ui-switch", { staticClass: "configcontrol", attrs: { label: _vm.param.label || _vm.param.name, title: "Default value: " + _vm.param.defaultValue }, domProps: { value: _vm.get() }, on: { input: _vm.set } }) : _vm.param.type == "DECIMAL" || _vm.param.type == "INTEGER" ? _c("input", { staticClass: "form-control configcontrol", attrs: { type: "number", min: _vm.param.min, max: _vm.param.max, title: "Default value: " + _vm.param.defaultValue }, domProps: { value: _vm.get() }, on: { input: _vm.set } }) : _vm.param.type == "TEXT" && ["time", "tel", "email", "url", "password"].includes( _vm.param.context ) ? _c("input", { staticClass: "form-control configcontrol", attrs: { type: _vm.param.context, title: "Default value: " + _vm.param.defaultValue }, domProps: { value: _vm.get() }, on: { input: _vm.set } }) : _vm.param.context == "tags" ? _c("ui-tags", { staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue }, on: { input: _vm.set } }) : _vm.param.type == "TEXT" && _vm.param.limitToOptions === true && _vm.param.options && _vm.param.options.length > 0 ? _c("ui-dropdown", { staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, placeholder: "Please select", valuekey: "value", viewkey: "label" }, domProps: { options: _vm.param.options }, on: { input: _vm.set } }) : _vm.param.type == "TEXT" && _vm.param.limitToOptions === true && _vm.param.options && _vm.param.options.length > 0 && _vm.param.multiple ? _c("ui-multiselect", { staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, placeholder: "Please select", valuekey: "value", viewkey: "label" }, domProps: { options: _vm.param.options }, on: { input: _vm.set } }) : _vm.param.context == "script" ? _c( "a", { staticClass: "btn btn-primary-hover configcontrol", attrs: { href: "#editor", title: "Default value: " + _vm.param.defaultValue }, on: { click: function($event) { return this.$emit("showeditor") } } }, [_vm._v("Edit script")] ) : _vm.param.context == "json" ? _c( "a", { staticClass: "btn btn-primary-hover configcontrol", attrs: { href: "#editor", title: "Default value: " + _vm.param.defaultValue }, on: { click: function($event) { return this.$emit("showeditor", { mimetype: "application/json" }) } } }, [_vm._v("Edit json")] ) : _vm.param.context == "date" ? _c("ui-time-picker", { staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue }, on: { input: _vm.set } }) : _vm.param.context == "datetime" ? _c("ui-time-picker", { staticClass: "configcontrol", attrs: { "enable-time": "true", value: _vm.get(), title: "Default value: " + _vm.param.defaultValue }, on: { input: _vm.set } }) : _vm.param.context == "rule" && _vm.param.multiple ? _c("ui-multiselect", { directives: [ { name: "dynamicload", rawName: "v-dynamicload:rules", value: _vm.param.filterCriteria, expression: "param.filterCriteria", arg: "rules" } ], staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, valuekey: "uid", viewkey: "name" }, on: { input: _vm.set } }) : _vm.param.context == "rule" ? _c("ui-dropdown", { directives: [ { name: "dynamicload", rawName: "v-dynamicload:rules", value: _vm.param.filterCriteria, expression: "param.filterCriteria", arg: "rules" } ], staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, valuekey: "uid", viewkey: "name" }, on: { input: _vm.set } }) : _vm.param.context == "channel" && _vm.param.multiple ? _c("ui-multiselect", { directives: [ { name: "dynamicload", rawName: "v-dynamicload:channels", value: _vm.param.filterCriteria, expression: "param.filterCriteria", arg: "channels" } ], staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, valuekey: "uid", viewkey: "label" }, on: { input: _vm.set } }) : _vm.param.context == "channel" ? _c("ui-dropdown", { directives: [ { name: "dynamicload", rawName: "v-dynamicload:channels", value: _vm.param.filterCriteria, expression: "param.filterCriteria", arg: "channels" } ], staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, valuekey: "uid", viewkey: "label" }, on: { input: _vm.set } }) : _vm.param.context == "item" && _vm.param.multiple ? _c("ui-multiselect", { directives: [ { name: "dynamicload", rawName: "v-dynamicload:items", value: _vm.param.filterCriteria, expression: "param.filterCriteria", arg: "items" } ], staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, valuekey: "name", viewkey: "label" }, on: { input: _vm.set } }) : _vm.param.context == "item" ? _c("ui-dropdown", { directives: [ { name: "dynamicload", rawName: "v-dynamicload:items", value: _vm.param.filterCriteria, expression: "param.filterCriteria", arg: "items" } ], staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue, valuekey: "name", desckey: "name", viewkey: "label" }, on: { input: _vm.set } }) : _vm.param.context == "cronexpression" ? _c("ui-cron-expression", { staticClass: "configcontrol", attrs: { value: _vm.get(), title: "Default value: " + _vm.param.defaultValue }, on: { input: _vm.set } }) : _vm.param.context == "location" ? _c( "div", { attrs: { title: "Default value: " + _vm.param.defaultValue } }, [ _c("input", { ref: "mapcoordinates", staticClass: "form-control configcontrol", domProps: { value: _vm.get() }, on: { input: _vm.set } }), _vm._v(" "), !_vm.condensed ? _c("ui-maps", { on: { change: _vm.mapChanged } }) : _vm._e() ], 1 ) : _c("input", { staticClass: "form-control configcontrol", attrs: { title: "Default value: " + _vm.param.defaultValue }, domProps: { value: _vm.get() }, on: { input: _vm.set } }), _vm._v(" "), _vm.desc ? _c("div", { staticClass: "configdesc", domProps: { innerHTML: _vm._s(_vm.param.description) } }) : _vm._e() ], 1 ) }; var __vue_staticRenderFns__ = []; __vue_render__._withStripped = true; /* style */ const __vue_inject_styles__ = undefined; /* scoped */ const __vue_scope_id__ = undefined; /* module identifier */ const __vue_module_identifier__ = undefined; /* functional template */ const __vue_is_functional_template__ = false; /* style inject */ /* style inject SSR */ var VueConfigElement = normalizeComponent( { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, undefined, undefined ); const replace = s => s.toLowerCase().replace(/ /g, '-'); const RuleNode = { template: '#rulenode', props: ['node', 'editor', 'bindSocket', 'bindControl'], components: { 'vue-config-element': VueConfigElement }, methods: { edit() { }, showEditor(control) { this.editor.trigger('showeditor', { control }); }, sanitizedName(name) { return replace(name); }, inputs() { return Array.from(this.node.inputs.values()) }, outputs() { return Array.from(this.node.outputs.values()) }, controls(n = null) { let v = Array.from(this.node.controls.values()); if (n) { this.hasmore |= v.length > n; return v.slice(0, n); } return v; }, selected() { return this.editor.selected.contains(this.node) ? 'selected' : ''; }, remove() { this.node.remove(); }, move(rel = 0) { let index = this.editor.nodes.findIndex(e => e == this.node); if (index == -1) return; const newindex = index + rel; // Check array bounds if (newindex < 0 || newindex > this.editor.nodes.length) return; // Check if the other element is still from the same type // (remember that we have triggers, conditions, actions and labels in the same nodes array.) if (this.editor.nodes[newindex].data.type != this.node.data.type) return; // Swap elements const temp = this.editor.nodes[newindex]; this.editor.nodes[newindex] = this.node; this.editor.nodes[index] = temp; this.editor.trigger('nodetranslated', { node: this.node }); console.log("MOVE", index, this.node, temp); } }, data: () => ({ islast: false, isfirst: false, hasmore: false }), directives: { // If a configuration uses this directive, the edit button // will be shown, even if all controls could be rendered. requirelarge: { bind() { this.hasmore = true; } }, socket: { bind(el, binding, vnode) { vnode.context.bindSocket(el, binding.arg, binding.value); } }, control: { bind(el, binding, vnode) { if (!binding.value) return; vnode.context.bindControl(el, binding.value); } } }, computed: { classes: function () { return [replace(this.selected()), this.node.data.type]; }, title: function () { return this.node.name + '\n' + this.node.data.hint; } } }; function createVue(el, vueComponent, vueProps) { const nodeEl = document.createElement('div'); el.appendChild(nodeEl); const app = Object.assign(new Vue(vueComponent), vueProps); app.$mount(nodeEl); return app; } function createNode(editor, { el, node, component, bindSocket, bindControl }) { const vueComponent = component.component || RuleNode; const vueProps = { ...component.props, node, editor, bindSocket, bindControl }; const app = createVue(el, vueComponent, vueProps); node.vueContext = app; if (node.mounted) node.mounted(); return app; } const update = (entity) => { if (entity.vueContext) entity.vueContext.$forceUpdate(); }; function install(editor, params) { editor.on('rendernode', ({ el, node, component, bindSocket, bindControl }) => { node.vueContext = createNode(editor, { el, node, component, bindSocket, bindControl }); node.update = () => update(node); }); editor.on('rendercontrol', ({ el, control }) => { //control.vueContext = createControl(editor, { el, control }); //control.update = () => update(control) }); editor.on('connectioncreated connectionremoved', connection => { update(connection.output.node); update(connection.input.node); }); editor.on('nodeselected', () => { editor.nodes.forEach(update); }); } var VueRenderPlugin = { name: 'vue-render', install }; function toTrainCase(str) { return str.toLowerCase().replace(/ /g, '-'); } function defaultPath(points, curvature) { const [x1, y1, x2, y2] = points; const hx1 = x1 + Math.abs(x2 - x1) * curvature; const hx2 = x2 - Math.abs(x2 - x1) * curvature; return `M ${x1} ${y1} C ${hx1} ${y1} ${hx2} ${y2} ${x2} ${y2}`; } function renderPathData(emitter, points, connection) { const data = { points, connection, d: '' }; emitter.trigger('connectionpath', data); return data.d || defaultPath(points, 0.4); } function updateConnection({ el, d }) { const path = el.querySelector('.connection path'); if (!path) throw new Error('Path of connection was broken'); path.setAttribute('d', d); } function renderConnection({ el, d, connection }) { const classed = !connection?[]:[ 'input-' + toTrainCase(connection.input.name), 'output-' + toTrainCase(connection.output.name), 'socket-input-' + toTrainCase(connection.input.socket.name), 'socket-output-' + toTrainCase(connection.output.socket.name) ]; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); svg.classList.add('connection', ...classed); path.classList.add('main-path'); path.setAttribute('d', d); svg.appendChild(path); el.appendChild(svg); updateConnection({ el, d }); } class Picker { constructor(editor) { this.el = document.createElement('div'); this.editor = editor; this._output = null; } get output() { return this._output; } set output(val) { const { area } = this.editor.view; this._output = val; if (val !== null) { area.appendChild(this.el); this.renderConnection(); } else if (this.el.parentElement) { area.removeChild(this.el); this.el.innerHTML = ''; } } getPoints() { const mouse = this.editor.view.area.mouse; const node = this.editor.view.nodes.get(this.output.node); const [x1, y1] = node.getSocketPosition(this.output); return [x1, y1, mouse.x, mouse.y]; } updateConnection() { if (!this.output) return; const d = renderPathData(this.editor, this.getPoints()); updateConnection({ el: this.el, d }); } renderConnection() { if (!this.output) return; const d = renderPathData(this.editor, this.getPoints()); renderConnection({ el: this.el, d, connection: null }); } } function install$1(editor) { editor.bind('connectionpath'); const picker = new Picker(editor); function pickOutput(output) { if (output && !picker.output) { picker.output = output; return; } } function pickInput(input) { if (picker.output === null) { if (input.hasConnection()) { picker.output = input.connections[0].output; editor.removeConnection(input.connections[0]); } return true; } if (!input.multipleConnections && input.hasConnection()) editor.removeConnection(input.connections[0]); if (!picker.output.multipleConnections && picker.output.hasConnection()) editor.removeConnection(picker.output.connections[0]); if (picker.output.connectedTo(input)) { const connection = input.connections.find(c => c.output === picker.output); editor.removeConnection(connection); } editor.connect(picker.output, input); picker.output = null; } function pickConnection(connection) { const { output } = connection; editor.removeConnection(connection); picker.output = output; } editor.on('rendersocket', ({ el, input, output }) => { let prevent = false; function mouseHandle(e) { if (prevent) return; e.stopPropagation(); e.preventDefault(); if (input) pickInput(input); else if (output) pickOutput(output); } el.addEventListener('mousedown', e => (mouseHandle(e), prevent = true)); el.addEventListener('mouseup', mouseHandle); el.addEventListener('click', e => (mouseHandle(e), prevent = false)); el.addEventListener('mousemove', () => (prevent = false)); }); editor.on('mousemove', () => { picker.updateConnection(); }); editor.view.container.addEventListener('mousedown', () => { picker.output = null; }); editor.on('renderconnection', ({ el, connection, points }) => { const d = renderPathData(editor, points, connection); el.addEventListener('contextmenu', e => { e.stopPropagation(); e.preventDefault(); pickConnection(connection); }); renderConnection({ el, d, connection }); }); editor.on('updateconnection', ({ el, connection, points }) => { const d = renderPathData(editor, points, connection); updateConnection({ el, connection, d }); }); } var ConnectionPlugin = { install: install$1, defaultPath }; const captionHeight = 32; const captionMargin = 16; const nodeGap = 64; /** * Responsible for importing OH rule json objects and exporting those objects. * * @param {NodeEditor} editor The rete editor * @param {Object} Settings The settings * @param {Number} Settings.size The grid pattern size * @category Rules * @memberof module:rule */ class LayoutManager { constructor(editor, { size = 16 }) { this.editor = editor; this.size = size; this.actionY = 0; // editor.on('nodetranslate', this._onTranslate.bind(this)) editor.on('nodecreated', this._onNodeCreated.bind(this)); editor.on('noderemoved', this._onNodeRemoved.bind(this)); editor.on('noderemove', this._onNodeRemove.bind(this)); editor.on('afterImport', this.relayout.bind(this)); editor.on('nodetranslated', this.fullrelayout.bind(this)); // editor.on('cleared', async () => await this.initEditor()); this.initEditor(); } dispose() { if (this.resizeBound) window.removeEventListener('resize', this.resizeBound, { passive: true }); if (this.debounceResizeTimer) clearInterval(this.debounceResizeTimer); delete this.debounceResizeTimer; this.editor.view.container.removeEventListener("dragover", this.boundDragover, true); this.editor.view.container.removeEventListener("drop", this.boundDrop, true); } /** * Creates the inital text nodes (labels) to show the user where the triggers, conditions and actions go. */ async initEditor() { this.nodes = { trigger: [], condition: [], action: [] }; this.editor.beforeImport(null, false); this.triggerCaption = this.editor.addNode(await this.editor.getComponent("trigger").createNode()); this.conditionCaption = this.editor.addNode(await this.editor.getComponent("condition").createNode()); this.actionCaption = this.editor.addNode(await this.editor.getComponent("action").createNode()); this.editor.afterImport(); if (this.resizeBound) window.removeEventListener('resize', this.resizeBound, { passive: true }); this.resizeBound = () => { if (this.debounceResizeTimer) { clearTimeout(this.debounceResizeTimer); delete this.debounceR