@feezal/feezal-element 0.6.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/feezal-conditions.js +262 -0
- package/feezal-element.js +208 -136
- package/feezal-polymer-element.js +326 -0
- package/package.json +36 -26
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019-2026 Sebastian Raff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @feezal/feezal-element
|
|
2
|
+
|
|
3
|
+
Base classes for [feezal](https://github.com/feezal/feezal) dashboard elements.
|
|
4
|
+
|
|
5
|
+
feezal is a browser-based MQTT dashboard editor and viewer. Dashboards are composed of web components (`feezal-element-*` packages); this package provides the base classes those elements extend:
|
|
6
|
+
|
|
7
|
+
- **`FeezalElement`** — the Lit 3 base class. MQTT subscription lifecycle (`addSubscription`, automatic unsubscribe), payload path extraction (`getProperty` / `message-property`), payload casting, the reserved `<subscribe>/setattribute|setstyle|addclass|…` runtime-control channel, and the per-element conditions engine.
|
|
8
|
+
- **`feezalBaseStyles`** — shared host styles every element composes into its `static styles`.
|
|
9
|
+
- **`html`, `css`** — re-exported from Lit so element packages need a single import.
|
|
10
|
+
- **`FeezalPolymerElement`** (`feezal-polymer-element.js`) — legacy Polymer 3 base used by the paper element family. New elements should extend `FeezalElement`.
|
|
11
|
+
- **`feezal-conditions.js`** — the shared conditions engine (declaratively bind visibility, classes, styles or attributes to MQTT topics). Wired up by both base classes; element authors get it for free.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @feezal/feezal-element
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Minimal element
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
/* global feezal */
|
|
23
|
+
import {FeezalElement, feezalBaseStyles, html, css} from '@feezal/feezal-element';
|
|
24
|
+
|
|
25
|
+
class FeezalElementMyWidget extends FeezalElement {
|
|
26
|
+
static get feezal() {
|
|
27
|
+
return {
|
|
28
|
+
palette: {name: 'My Widget', category: 'Basic', color: '#4a6080'},
|
|
29
|
+
attributes: [
|
|
30
|
+
{name: 'subscribe', type: 'mqttTopic', help: 'State topic.'},
|
|
31
|
+
{name: 'message-property', type: 'string', default: 'payload',
|
|
32
|
+
help: 'Dot-notation path to the value within the MQTT message.'},
|
|
33
|
+
],
|
|
34
|
+
styles: ['top', 'left', 'width', 'height'],
|
|
35
|
+
defaultStyle: {width: '80px', height: '40px'},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static properties = {
|
|
40
|
+
_value: {state: true},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
static styles = [feezalBaseStyles, css``];
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
this._value = null; // reactive properties: constructor, never class fields
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
connectedCallback() {
|
|
51
|
+
super.connectedCallback();
|
|
52
|
+
if (this.subscribe) {
|
|
53
|
+
this.addSubscription(this.subscribe, msg => {
|
|
54
|
+
this._value = this.getProperty(msg, this.messageProperty);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
return html`<div>${this._value}</div>`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
customElements.define('feezal-element-my-widget', FeezalElementMyWidget);
|
|
65
|
+
export {FeezalElementMyWidget};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Authoring elements
|
|
69
|
+
|
|
70
|
+
Read the full element authoring specification before building anything real — descriptor reference (`palette`, `attributes`, `styles`, `discovery`, custom inspectors), MQTT topic conventions, CSS custom property conventions, Lit 3 gotchas and the publishing checklist:
|
|
71
|
+
|
|
72
|
+
**[docs/element-spec.md](https://github.com/feezal/feezal/blob/master/docs/element-spec.md)**
|
|
73
|
+
|
|
74
|
+
Element packages are named `feezal-element-<category>-<name>` under an npm scope; the categories `basic` and `paper` are reserved for official feezal elements. Scaffolding: [`create-feezal-element`](https://github.com/feezal/feezal/tree/master/packages/create-feezal-element).
|
|
75
|
+
|
|
76
|
+
## Versioning
|
|
77
|
+
|
|
78
|
+
This package uses lockstep **major** versions with the feezal server and the official element packages; minor/patch versions are independent. Element packages should depend on the matching major, e.g. `"@feezal/feezal-element": "^3.0.0"`.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT. The base class and the viewer runtime your element runs inside are MIT-licensed — subclassing does **not** place your element under feezal's AGPL (which covers only the server and editor). You may license and distribute your element packages under any terms you choose.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/* global feezal */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* E50 — per-element conditions engine.
|
|
5
|
+
*
|
|
6
|
+
* Every element can carry a `conditions` attribute holding a JSON list of
|
|
7
|
+
* rows that declaratively bind visibility, CSS classes, styles or attributes
|
|
8
|
+
* to MQTT topics (or page-locally published ones, E49):
|
|
9
|
+
*
|
|
10
|
+
* [{"subscribe": "home/alarm/state", "operator": "=", "value": "armed",
|
|
11
|
+
* "action": "attribute", "attribute": "disabled", "attribute-value": ""}]
|
|
12
|
+
*
|
|
13
|
+
* Row schema:
|
|
14
|
+
* subscribe topic (exact or `${param}` placeholder inside components)
|
|
15
|
+
* property optional dot-path into the message (default "payload")
|
|
16
|
+
* operator = | != | > | < | >= | <= | matches (default =)
|
|
17
|
+
* value compare value (regex source for `matches`)
|
|
18
|
+
* action show | hide | class | style | attribute
|
|
19
|
+
* class (action=class) class name to add while matched
|
|
20
|
+
* style (action=style) {prop: value, …} applied while matched
|
|
21
|
+
* attribute (action=attribute) attribute name
|
|
22
|
+
* attribute-value (action=attribute) value set while matched
|
|
23
|
+
* keep-layout (action=show/hide) use visibility:hidden instead of
|
|
24
|
+
* display:none so the element keeps its layout box
|
|
25
|
+
*
|
|
26
|
+
* Semantics (decided in the E50 roadmap entry):
|
|
27
|
+
* - rows evaluate independently against the last value on their topic;
|
|
28
|
+
* - visibility rows AND-combine — visible only if every `show` row matches
|
|
29
|
+
* and no `hide` row matches;
|
|
30
|
+
* - class/style/attribute rows apply while matched and revert when
|
|
31
|
+
* unmatched; several matching rows targeting the same style property or
|
|
32
|
+
* attribute: the later row wins;
|
|
33
|
+
* - reverting restores the element's pristine value (captured before any
|
|
34
|
+
* condition effect was applied);
|
|
35
|
+
* - effects are never applied in the editor (a badge shows instead).
|
|
36
|
+
*
|
|
37
|
+
* The engine is host-agnostic so both element base classes (Lit
|
|
38
|
+
* FeezalElement and Polymer FeezalPolymerElement) share it.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const OPERATORS = ['=', '!=', '>', '<', '>=', '<=', 'matches'];
|
|
42
|
+
const ACTIONS = ['show', 'hide', 'class', 'style', 'attribute'];
|
|
43
|
+
|
|
44
|
+
/** Parse + validate a conditions attribute value. Garbage in → []. */
|
|
45
|
+
export function parseConditions(raw) {
|
|
46
|
+
if (!raw || typeof raw !== 'string') return [];
|
|
47
|
+
let list;
|
|
48
|
+
try {
|
|
49
|
+
list = JSON.parse(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!Array.isArray(list)) return [];
|
|
55
|
+
return list.filter(row =>
|
|
56
|
+
row && typeof row === 'object' &&
|
|
57
|
+
typeof row.subscribe === 'string' && row.subscribe.length > 0 &&
|
|
58
|
+
ACTIONS.includes(row.action) &&
|
|
59
|
+
(row.operator === undefined || OPERATORS.includes(row.operator)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Evaluate one condition against a payload value. Pure. */
|
|
63
|
+
export function evalCondition(operator, compareValue, payload) {
|
|
64
|
+
const op = operator || '=';
|
|
65
|
+
switch (op) {
|
|
66
|
+
case '=':
|
|
67
|
+
return String(payload) === String(compareValue);
|
|
68
|
+
case '!=':
|
|
69
|
+
return String(payload) !== String(compareValue);
|
|
70
|
+
case '>':
|
|
71
|
+
case '<':
|
|
72
|
+
case '>=':
|
|
73
|
+
case '<=': {
|
|
74
|
+
const a = Number(payload);
|
|
75
|
+
const b = Number(compareValue);
|
|
76
|
+
if (isNaN(a) || isNaN(b)) return false;
|
|
77
|
+
if (op === '>') return a > b;
|
|
78
|
+
if (op === '<') return a < b;
|
|
79
|
+
if (op === '>=') return a >= b;
|
|
80
|
+
return a <= b;
|
|
81
|
+
}
|
|
82
|
+
case 'matches':
|
|
83
|
+
try {
|
|
84
|
+
return new RegExp(String(compareValue)).test(String(payload));
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class FeezalConditions {
|
|
94
|
+
/** @param {HTMLElement} host element the effects apply to (needs getProperty()) */
|
|
95
|
+
constructor(host) {
|
|
96
|
+
this.host = host;
|
|
97
|
+
this._subs = [];
|
|
98
|
+
this._rows = [];
|
|
99
|
+
this._matched = [];
|
|
100
|
+
this._pristine = null;
|
|
101
|
+
this._active = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* (Re)start the engine from the host's current `conditions` attribute.
|
|
106
|
+
* No-ops in the editor (effects are editor-badged, never applied) and
|
|
107
|
+
* when the attribute is absent/empty/invalid.
|
|
108
|
+
*/
|
|
109
|
+
connect() {
|
|
110
|
+
const raw = this.host.getAttribute('conditions');
|
|
111
|
+
// Idempotent for an unchanged attribute — connectedCallback and the
|
|
112
|
+
// first Lit update may both call connect() for the same value.
|
|
113
|
+
if (this._active && raw === this._lastRaw) return;
|
|
114
|
+
|
|
115
|
+
this.disconnect();
|
|
116
|
+
this._lastRaw = raw;
|
|
117
|
+
if (typeof feezal === 'undefined' || feezal.isEditor) return;
|
|
118
|
+
|
|
119
|
+
this._rows = parseConditions(raw);
|
|
120
|
+
if (this._rows.length === 0) return;
|
|
121
|
+
|
|
122
|
+
this._matched = this._rows.map(() => false);
|
|
123
|
+
this._capturePristine();
|
|
124
|
+
this._active = true;
|
|
125
|
+
|
|
126
|
+
this._rows.forEach((row, i) => {
|
|
127
|
+
this._subs.push(feezal.connection.sub(row.subscribe, msg => {
|
|
128
|
+
const payload = this.host.getProperty
|
|
129
|
+
? this.host.getProperty(msg, row.property || 'payload')
|
|
130
|
+
: (msg && typeof msg === 'object' ? msg.payload : msg);
|
|
131
|
+
this._matched[i] = evalCondition(row.operator, row.value, payload);
|
|
132
|
+
this._apply();
|
|
133
|
+
}));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Stop: unsubscribe and revert every applied effect. */
|
|
138
|
+
disconnect() {
|
|
139
|
+
for (const sub of this._subs.splice(0)) {
|
|
140
|
+
feezal.connection.unsubscribe(sub);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this._active) {
|
|
144
|
+
// Revert to pristine so a re-connect (element moved in the DOM,
|
|
145
|
+
// conditions edited) starts from a clean slate.
|
|
146
|
+
this._matched = this._rows.map(() => false);
|
|
147
|
+
this._apply();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this._rows = [];
|
|
151
|
+
this._matched = [];
|
|
152
|
+
this._pristine = null;
|
|
153
|
+
this._active = false;
|
|
154
|
+
this._lastRaw = undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Record the element's pre-condition values for everything rows may touch. */
|
|
158
|
+
_capturePristine() {
|
|
159
|
+
const host = this.host;
|
|
160
|
+
const p = {styles: {}, attrs: {}, display: null, visibility: null};
|
|
161
|
+
|
|
162
|
+
for (const row of this._rows) {
|
|
163
|
+
if (row.action === 'style' && row.style && typeof row.style === 'object') {
|
|
164
|
+
for (const prop of Object.keys(row.style)) {
|
|
165
|
+
if (!(prop in p.styles)) p.styles[prop] = host.style.getPropertyValue(prop);
|
|
166
|
+
}
|
|
167
|
+
} else if (row.action === 'attribute' && row.attribute) {
|
|
168
|
+
if (!(row.attribute in p.attrs)) {
|
|
169
|
+
p.attrs[row.attribute] = {
|
|
170
|
+
had: host.hasAttribute(row.attribute),
|
|
171
|
+
value: host.getAttribute(row.attribute),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
} else if (row.action === 'show' || row.action === 'hide') {
|
|
175
|
+
if (p.display === null) {
|
|
176
|
+
p.display = host.style.getPropertyValue('display');
|
|
177
|
+
p.visibility = host.style.getPropertyValue('visibility');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this._pristine = p;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Re-derive and apply the full effect state from the per-row match flags. */
|
|
186
|
+
_apply() {
|
|
187
|
+
const host = this.host;
|
|
188
|
+
const p = this._pristine;
|
|
189
|
+
|
|
190
|
+
// ── classes: add while matched, remove when unmatched (row order) ──
|
|
191
|
+
this._rows.forEach((row, i) => {
|
|
192
|
+
if (row.action === 'class' && row.class) {
|
|
193
|
+
if (this._matched[i]) host.classList.add(row.class);
|
|
194
|
+
else host.classList.remove(row.class);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── visibility: AND-combine show/hide rows ──
|
|
199
|
+
if (p.display !== null) {
|
|
200
|
+
const visible = this._rows.every((row, i) => {
|
|
201
|
+
if (row.action === 'show') return this._matched[i];
|
|
202
|
+
if (row.action === 'hide') return !this._matched[i];
|
|
203
|
+
return true;
|
|
204
|
+
});
|
|
205
|
+
const keepLayout = this._rows.some((row, i) =>
|
|
206
|
+
(row.action === 'show' || row.action === 'hide') && row['keep-layout'] &&
|
|
207
|
+
(row.action === 'show' ? !this._matched[i] : this._matched[i]));
|
|
208
|
+
|
|
209
|
+
if (visible) {
|
|
210
|
+
// Restore pristine inline values ('' removes the property).
|
|
211
|
+
this._setOrRemoveStyle('display', p.display);
|
|
212
|
+
this._setOrRemoveStyle('visibility', p.visibility);
|
|
213
|
+
} else if (keepLayout) {
|
|
214
|
+
this._setOrRemoveStyle('display', p.display);
|
|
215
|
+
host.style.setProperty('visibility', 'hidden');
|
|
216
|
+
} else {
|
|
217
|
+
this._setOrRemoveStyle('visibility', p.visibility);
|
|
218
|
+
host.style.setProperty('display', 'none');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── styles: desired map from matching rows in order (later wins) ──
|
|
223
|
+
const desiredStyles = {};
|
|
224
|
+
this._rows.forEach((row, i) => {
|
|
225
|
+
if (row.action === 'style' && this._matched[i] && row.style && typeof row.style === 'object') {
|
|
226
|
+
Object.assign(desiredStyles, row.style);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
for (const [prop, pristineValue] of Object.entries(p.styles)) {
|
|
230
|
+
if (prop in desiredStyles) {
|
|
231
|
+
host.style.setProperty(prop, String(desiredStyles[prop]));
|
|
232
|
+
} else {
|
|
233
|
+
this._setOrRemoveStyle(prop, pristineValue);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── attributes: desired map from matching rows in order (later wins) ──
|
|
238
|
+
const desiredAttrs = {};
|
|
239
|
+
this._rows.forEach((row, i) => {
|
|
240
|
+
if (row.action === 'attribute' && this._matched[i] && row.attribute) {
|
|
241
|
+
desiredAttrs[row.attribute] = row['attribute-value'] ?? '';
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
for (const [name, pristine] of Object.entries(p.attrs)) {
|
|
245
|
+
if (name in desiredAttrs) {
|
|
246
|
+
host.setAttribute(name, String(desiredAttrs[name]));
|
|
247
|
+
} else if (pristine.had) {
|
|
248
|
+
host.setAttribute(name, pristine.value);
|
|
249
|
+
} else {
|
|
250
|
+
host.removeAttribute(name);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_setOrRemoveStyle(prop, value) {
|
|
256
|
+
if (value) {
|
|
257
|
+
this.host.style.setProperty(prop, value);
|
|
258
|
+
} else {
|
|
259
|
+
this.host.style.removeProperty(prop);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
package/feezal-element.js
CHANGED
|
@@ -1,113 +1,185 @@
|
|
|
1
|
-
/* global
|
|
1
|
+
/* global feezal */
|
|
2
|
+
import {LitElement, html, css} from 'lit';
|
|
3
|
+
import {FeezalConditions} from './feezal-conditions.js';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Shared base styles — identical to the Polymer dom-module 'feezal-style-element'.
|
|
7
|
+
* Exported so subclasses can compose via:
|
|
8
|
+
* static styles = [FeezalElementLit.styles, css`...additional...`];
|
|
9
|
+
*/
|
|
10
|
+
export const feezalBaseStyles = css`
|
|
11
|
+
:host {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
:host([hidden]) {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
:host(.feezal-editable) {
|
|
20
|
+
outline: 1px dashed rgba(var(--feezal-selection-rgb, 2,132,199), 0.8);
|
|
21
|
+
}
|
|
22
|
+
:host(.feezal-editable) * {
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
25
|
+
:host(.feezal-editable.feezal-selected) {
|
|
26
|
+
outline: 2px dashed rgba(var(--feezal-selection-rgb, 2,132,199), 0.9);
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
4
29
|
|
|
5
|
-
|
|
30
|
+
/**
|
|
31
|
+
* FeezalElement — Lit base class for feezal elements.
|
|
32
|
+
*
|
|
33
|
+
* A faithful port of the Polymer-based FeezalPolymerElement to Lit 3.
|
|
34
|
+
* Paper elements keep importing the Polymer FeezalPolymerElement unchanged;
|
|
35
|
+
* only the basic-* elements use this class.
|
|
36
|
+
*
|
|
37
|
+
* Key differences from the Polymer base:
|
|
38
|
+
* - Subclass CSS goes in static styles = [FeezalElement.styles, css`...`]
|
|
39
|
+
* - Polymer observers become updated(changed) guards
|
|
40
|
+
* - this.$.id becomes this.renderRoot.querySelector('#id')
|
|
41
|
+
* - Lit needs explicit attribute: 'kebab-name' for camelCase properties
|
|
42
|
+
*/
|
|
43
|
+
export class FeezalElement extends LitElement {
|
|
44
|
+
static styles = feezalBaseStyles;
|
|
6
45
|
|
|
7
|
-
styleElement.innerHTML =
|
|
8
|
-
`<template>
|
|
9
|
-
<style>
|
|
10
|
-
:host {
|
|
11
|
-
display: inline-block;
|
|
12
|
-
box-sizing: border-box;
|
|
13
|
-
overflow: hidden;
|
|
14
|
-
}
|
|
15
|
-
:host([hidden]) {
|
|
16
|
-
display: none;
|
|
17
|
-
}
|
|
18
|
-
:host(.feezal-editable) {
|
|
19
|
-
outline: 1px dashed rgba(250, 120, 0, 0.8);
|
|
20
|
-
}
|
|
21
|
-
:host(.feezal-editable.feezal-selected) {
|
|
22
|
-
outline: 2px dashed rgba(250, 120, 0, 0.8);
|
|
23
|
-
}
|
|
24
|
-
.feezal-blocker {
|
|
25
|
-
top: 0;
|
|
26
|
-
left: 0;
|
|
27
|
-
display: block;
|
|
28
|
-
width: 100%;
|
|
29
|
-
height: 100%;
|
|
30
|
-
position: absolute;
|
|
31
|
-
z-index: 10;
|
|
32
|
-
}
|
|
33
|
-
</style>
|
|
34
|
-
</template>`;
|
|
35
|
-
|
|
36
|
-
styleElement.register('feezal-style-element');
|
|
37
|
-
|
|
38
|
-
class FeezalElement extends PolymerElement {
|
|
39
|
-
static get properties() {
|
|
40
|
-
return {
|
|
41
|
-
'selected': {
|
|
42
|
-
type: Boolean,
|
|
43
|
-
value: false
|
|
44
|
-
},
|
|
45
|
-
'dynamicSubscriptions': {
|
|
46
|
-
type: Boolean,
|
|
47
|
-
value: false,
|
|
48
|
-
reflectToAttribute: true
|
|
49
|
-
},
|
|
50
|
-
'visible': {
|
|
51
|
-
type: Boolean,
|
|
52
|
-
value: false,
|
|
53
|
-
observer: '_visibleChanged'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
46
|
static get feezal() {
|
|
58
|
-
return {
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
return {attributes: [], styles: []};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static properties = {
|
|
51
|
+
// NOTE: Lit does NOT auto-convert camelCase to kebab-case for attribute
|
|
52
|
+
// names (unlike Polymer). Each camelCase property needs an explicit
|
|
53
|
+
// attribute option so that saved views (which store kebab-case) are
|
|
54
|
+
// read back correctly.
|
|
55
|
+
subscribe: {type: String, reflect: true, attribute: 'subscribe'},
|
|
56
|
+
messageProperty: {type: String, reflect: true, attribute: 'message-property'},
|
|
57
|
+
dynamicSubscriptions: {type: Boolean, reflect: true, attribute: 'dynamic-subscriptions'},
|
|
58
|
+
visible: {type: Boolean, reflect: true},
|
|
59
|
+
// E50: JSON list of condition rows (visibility/class/style/attribute
|
|
60
|
+
// bound to MQTT topics). Never reflected — the attribute is the
|
|
61
|
+
// source of truth; effects only apply in the viewer.
|
|
62
|
+
conditions: {type: String, attribute: 'conditions'},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
constructor() {
|
|
66
|
+
super();
|
|
67
|
+
this._subscriptions = [];
|
|
68
|
+
this._subscribed = false;
|
|
69
|
+
this.messageProperty = 'payload';
|
|
70
|
+
this.dynamicSubscriptions = false;
|
|
71
|
+
this.visible = false;
|
|
72
|
+
this._conditions = new FeezalConditions(this);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
connectedCallback() {
|
|
76
|
+
super.connectedCallback();
|
|
77
|
+
this.classList.add('feezal-element');
|
|
78
|
+
if (this.visible || !this.dynamicSubscriptions) {
|
|
79
|
+
this._subscribe();
|
|
80
|
+
this._conditions.connect();
|
|
61
81
|
}
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
super.disconnectedCallback();
|
|
86
|
+
this._unsubscribe();
|
|
87
|
+
this._conditions.disconnect();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
updated(changed) {
|
|
91
|
+
if (changed.has('visible') && this.dynamicSubscriptions) {
|
|
92
|
+
if (this.visible) {
|
|
93
|
+
this._subscribe();
|
|
94
|
+
this._conditions.connect();
|
|
95
|
+
} else {
|
|
96
|
+
this._unsubscribe();
|
|
97
|
+
this._conditions.disconnect();
|
|
71
98
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// E50: conditions added/edited/removed at runtime → restart the
|
|
102
|
+
// engine. connect() is idempotent for an unchanged attribute, so the
|
|
103
|
+
// first update after mount is a no-op.
|
|
104
|
+
if (changed.has('conditions') && (this.visible || !this.dynamicSubscriptions)) {
|
|
105
|
+
this._conditions.connect();
|
|
74
106
|
}
|
|
75
107
|
}
|
|
76
108
|
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
// ── Subscription helpers ─────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/** Subscribe to a single topic — convenience wrapper for subclasses. */
|
|
112
|
+
addSubscription(topic, callback) {
|
|
113
|
+
this._subscriptions.push(feezal.connection.sub(topic, callback));
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
_subscribe() {
|
|
82
|
-
|
|
117
|
+
// The _subscribed guard makes this idempotent: an element that mounts
|
|
118
|
+
// with visible already set (the attribute reflects, so it can arrive
|
|
119
|
+
// from saved view HTML) hits _subscribe from both connectedCallback
|
|
120
|
+
// and the first updated() — without the guard every topic would be
|
|
121
|
+
// subscribed twice and each message processed twice.
|
|
122
|
+
if (!this.subscribe || this._subscribed) {
|
|
83
123
|
return;
|
|
84
124
|
}
|
|
85
125
|
|
|
126
|
+
// In the editor, runtime MQTT manipulation of elements is gated by a user
|
|
127
|
+
// setting (Editor settings → "Prevent MQTT element manipulation in editor",
|
|
128
|
+
// default ON) so live broker values never get written onto elements and
|
|
129
|
+
// serialized into the saved view.
|
|
130
|
+
if (feezal.isEditor && feezal.preventEditorMqtt !== false) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this._subscribed = true;
|
|
135
|
+
|
|
136
|
+
const base = this.subscribe;
|
|
86
137
|
const elemClass = window.customElements.get(this.localName);
|
|
87
138
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
139
|
+
// Primary state topic → baseAttribute (exact topic, no wildcard).
|
|
140
|
+
const baseAttribute = elemClass?.feezal?.baseAttribute;
|
|
141
|
+
if (baseAttribute) {
|
|
142
|
+
this._subscriptions.push(feezal.connection.sub(base, msg => {
|
|
143
|
+
const type = (elemClass.properties?.[baseAttribute] || {}).type;
|
|
144
|
+
const val = this._payloadCast(type, this.getProperty(msg, this.messageProperty));
|
|
145
|
+
if (type === Boolean && !val) {
|
|
146
|
+
this.removeAttribute(baseAttribute);
|
|
147
|
+
} else {
|
|
148
|
+
this.setAttribute(baseAttribute, val);
|
|
149
|
+
}
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
95
152
|
|
|
96
|
-
|
|
153
|
+
// Reserved runtime-control channel — distinct, exact topics so device
|
|
154
|
+
// telemetry sharing the base topic can never reach the element.
|
|
155
|
+
// Consistent with feezal-view / feezal-site addclass / removeclass.
|
|
156
|
+
this._subscribeControl(base);
|
|
157
|
+
}
|
|
97
158
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
159
|
+
/** Subscribe to the reserved <base>/{setattribute,removeattribute,setstyle,removestyle,addclass,removeclass} control topics. */
|
|
160
|
+
_subscribeControl(base) {
|
|
161
|
+
const sub = (suffix, cb) => this._subscriptions.push(feezal.connection.sub(base + '/' + suffix, cb));
|
|
162
|
+
const val = msg => this.getProperty(msg, this.messageProperty);
|
|
163
|
+
const list = p => (Array.isArray(p) ? p : String(p).split(/[,\s]+/)).filter(Boolean);
|
|
164
|
+
|
|
165
|
+
sub('setattribute', msg => {
|
|
166
|
+
const obj = val(msg);
|
|
167
|
+
if (obj && typeof obj === 'object') {
|
|
168
|
+
for (const [k, v] of Object.entries(obj)) this.setAttribute(k, String(v));
|
|
106
169
|
}
|
|
107
|
-
})
|
|
170
|
+
});
|
|
171
|
+
sub('removeattribute', msg => list(val(msg)).forEach(n => this.removeAttribute(n)));
|
|
172
|
+
sub('setstyle', msg => {
|
|
173
|
+
const obj = val(msg);
|
|
174
|
+
if (obj && typeof obj === 'object') Object.assign(this.style, obj);
|
|
175
|
+
});
|
|
176
|
+
sub('removestyle', msg => list(val(msg)).forEach(n => this.style.removeProperty(n)));
|
|
177
|
+
sub('addclass', msg => this.classList.add(val(msg)));
|
|
178
|
+
sub('removeclass', msg => this.classList.remove(val(msg)));
|
|
108
179
|
}
|
|
109
180
|
|
|
110
181
|
_unsubscribe() {
|
|
182
|
+
this._subscribed = false;
|
|
111
183
|
const sub = this._subscriptions.shift();
|
|
112
184
|
if (sub) {
|
|
113
185
|
feezal.connection.unsubscribe(sub);
|
|
@@ -115,80 +187,80 @@ class FeezalElement extends PolymerElement {
|
|
|
115
187
|
}
|
|
116
188
|
}
|
|
117
189
|
|
|
118
|
-
|
|
119
|
-
super();
|
|
120
|
-
this._subscriptions = [];
|
|
121
|
-
}
|
|
190
|
+
// ── Utilities (ported 1:1 from the Polymer base) ─────────────────────────
|
|
122
191
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (feezal.isEditor) {
|
|
127
|
-
const el = document.createElement('div');
|
|
128
|
-
el.className = 'feezal-blocker';
|
|
129
|
-
this.shadowRoot.prepend(el);
|
|
130
|
-
this.shadowRoot.querySelectorAll('*').forEach(el => {
|
|
131
|
-
if (el.hasAttribute('tabindex')) {
|
|
132
|
-
el.addEventListener('focus', event => {
|
|
133
|
-
event.preventDefault();
|
|
134
|
-
if (event.relatedTarget) {
|
|
135
|
-
event.relatedTarget.focus();
|
|
136
|
-
} else {
|
|
137
|
-
event.currentTarget.blur();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
el.setAttribute('tabindex', '-1');
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
} else {
|
|
144
|
-
if (this.visible || !this.dynamicSubscriptions) {
|
|
145
|
-
this._subscribe();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
disconnectedCallback() {
|
|
150
|
-
super.disconnectedCallback();
|
|
151
|
-
this._unsubscribe();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
_visibleChanged(visible) {
|
|
155
|
-
if (feezal.isEditor || !this.dynamicSubscriptions) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (visible) {
|
|
159
|
-
this._subscribe();
|
|
160
|
-
} else {
|
|
161
|
-
this._unsubscribe();
|
|
192
|
+
_payloadCast(type, payload) {
|
|
193
|
+
if (typeof payload === 'string' && type === Boolean) {
|
|
194
|
+
return Number(payload) !== 0 && payload.toLowerCase() !== 'false';
|
|
162
195
|
}
|
|
196
|
+
return payload;
|
|
163
197
|
}
|
|
164
198
|
|
|
165
|
-
// Utils
|
|
166
199
|
throttle(func, limit) {
|
|
167
200
|
let lastFunc;
|
|
168
201
|
let lastRan;
|
|
169
202
|
return function () {
|
|
170
203
|
const context = this;
|
|
171
|
-
const args
|
|
172
|
-
|
|
204
|
+
const args = arguments;
|
|
173
205
|
if (!lastRan) {
|
|
174
206
|
func.apply(context, args);
|
|
175
207
|
lastRan = Date.now();
|
|
176
208
|
} else {
|
|
177
209
|
clearTimeout(lastFunc);
|
|
178
|
-
this._inThrottle = true;
|
|
179
210
|
lastFunc = setTimeout(function () {
|
|
180
211
|
if ((Date.now() - lastRan) >= limit) {
|
|
181
212
|
func.apply(context, args);
|
|
182
213
|
lastRan = Date.now();
|
|
183
|
-
this._inThrottle = false;
|
|
184
214
|
}
|
|
185
215
|
}, limit - (Date.now() - lastRan));
|
|
186
216
|
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
split(str) {
|
|
221
|
+
str = String(str);
|
|
222
|
+
if (str.indexOf('\\') === -1) {
|
|
223
|
+
return str.split('.');
|
|
224
|
+
}
|
|
225
|
+
const res = [];
|
|
226
|
+
let pos = 0;
|
|
227
|
+
function chunk(start, end) {
|
|
228
|
+
res.push(str.slice(start, end).replace(/\\\\/g, '\\').replace(/\\\./g, '.'));
|
|
229
|
+
pos = end + 1;
|
|
230
|
+
}
|
|
231
|
+
let esc, j;
|
|
232
|
+
const l = str.length;
|
|
233
|
+
let i;
|
|
234
|
+
for (i = 0; i < l; i++) {
|
|
235
|
+
if (str[i] === '.') {
|
|
236
|
+
esc = false;
|
|
237
|
+
for (j = i - 1; str[j] === '\\'; j--) {
|
|
238
|
+
esc = !esc;
|
|
239
|
+
}
|
|
240
|
+
if (!esc) {
|
|
241
|
+
chunk(pos, i);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
187
244
|
}
|
|
245
|
+
chunk(pos, i);
|
|
246
|
+
return res;
|
|
188
247
|
}
|
|
189
248
|
|
|
249
|
+
getProperty(obj, prop) {
|
|
250
|
+
const type = typeof obj;
|
|
251
|
+
if (type !== 'object' && type !== 'function') {
|
|
252
|
+
return typeof prop === 'undefined' ? obj : undefined;
|
|
253
|
+
}
|
|
254
|
+
const arr = this.split(String(prop));
|
|
255
|
+
let res = obj;
|
|
256
|
+
for (let i = 0, l = arr.length; i < l; i++) {
|
|
257
|
+
if (res) {
|
|
258
|
+
res = res[arr[i]];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return res;
|
|
262
|
+
}
|
|
190
263
|
}
|
|
191
264
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
export {FeezalElement, html};
|
|
265
|
+
// Re-export so element files can do a single import.
|
|
266
|
+
export {html, css};
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/* global window, feezal */
|
|
2
|
+
|
|
3
|
+
import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
|
|
4
|
+
import {FeezalConditions} from './feezal-conditions.js';
|
|
5
|
+
|
|
6
|
+
const styleElement = document.createElement('dom-module');
|
|
7
|
+
|
|
8
|
+
styleElement.innerHTML =
|
|
9
|
+
`<template>
|
|
10
|
+
<style>
|
|
11
|
+
:host {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
:host([hidden]) {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
:host(.feezal-editable) {
|
|
20
|
+
outline: 1px dashed rgba(var(--feezal-selection-rgb, 2,132,199), 0.8);
|
|
21
|
+
}
|
|
22
|
+
:host(.feezal-editable) * {
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
25
|
+
:host(.feezal-editable.feezal-selected) {
|
|
26
|
+
outline: 2px dashed rgba(var(--feezal-selection-rgb, 2,132,199), 0.9);
|
|
27
|
+
}
|
|
28
|
+
:host(.feezal-editable[locked]) {
|
|
29
|
+
outline: 1.5px dashed #f59e0b !important;
|
|
30
|
+
}
|
|
31
|
+
:host(.feezal-editable.feezal-selected[locked]) {
|
|
32
|
+
outline: 2px solid #f59e0b !important;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
35
|
+
</template>`;
|
|
36
|
+
|
|
37
|
+
styleElement.register('feezal-style-element');
|
|
38
|
+
|
|
39
|
+
class FeezalPolymerElement extends PolymerElement {
|
|
40
|
+
static get properties() {
|
|
41
|
+
return {
|
|
42
|
+
name: {
|
|
43
|
+
type: String,
|
|
44
|
+
value: '',
|
|
45
|
+
reflectToAttribute: true,
|
|
46
|
+
},
|
|
47
|
+
'selected': {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
value: false
|
|
50
|
+
},
|
|
51
|
+
subscribe: {
|
|
52
|
+
type: String,
|
|
53
|
+
value: '',
|
|
54
|
+
reflectToAttribute: true,
|
|
55
|
+
},
|
|
56
|
+
messageProperty: {
|
|
57
|
+
type: String,
|
|
58
|
+
value: 'payload',
|
|
59
|
+
reflectToAttribute: true,
|
|
60
|
+
},
|
|
61
|
+
'dynamicSubscriptions': {
|
|
62
|
+
type: Boolean,
|
|
63
|
+
value: false,
|
|
64
|
+
reflectToAttribute: true
|
|
65
|
+
},
|
|
66
|
+
'visible': {
|
|
67
|
+
type: Boolean,
|
|
68
|
+
value: false,
|
|
69
|
+
observer: '_visibleChanged'
|
|
70
|
+
},
|
|
71
|
+
// E50: JSON list of condition rows — shared engine, see
|
|
72
|
+
// feezal-conditions.js. The attribute is the source of truth.
|
|
73
|
+
conditions: {
|
|
74
|
+
type: String,
|
|
75
|
+
observer: '_conditionsChanged'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
static get feezal() {
|
|
80
|
+
return {
|
|
81
|
+
attributes: [],
|
|
82
|
+
styles: []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_payloadCast(type, payload) {
|
|
87
|
+
if (typeof payload === 'string') {
|
|
88
|
+
switch (type) {
|
|
89
|
+
case Boolean:
|
|
90
|
+
return Number(payload) !== 0 && payload.toLowerCase() !== 'false';
|
|
91
|
+
default:
|
|
92
|
+
return payload;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
return payload;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
addSubscription(topic, callback) {
|
|
100
|
+
this._subscriptions.push(feezal.connection.sub(topic, callback));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_subscribe() {
|
|
104
|
+
if (!this.subscribe) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// In the editor, runtime MQTT manipulation of elements is gated by a user
|
|
109
|
+
// setting (Editor settings → "Prevent MQTT element manipulation in editor",
|
|
110
|
+
// default ON) so live broker values never get written onto elements and
|
|
111
|
+
// serialized into the saved view.
|
|
112
|
+
if (feezal.isEditor && feezal.preventEditorMqtt !== false) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const base = this.subscribe;
|
|
117
|
+
const elemClass = window.customElements.get(this.localName);
|
|
118
|
+
|
|
119
|
+
// Primary state topic → baseAttribute (exact topic, no wildcard).
|
|
120
|
+
const baseAttribute = elemClass && elemClass.feezal && elemClass.feezal.baseAttribute;
|
|
121
|
+
if (baseAttribute) {
|
|
122
|
+
this._subscriptions.push(feezal.connection.sub(base, msg => {
|
|
123
|
+
const type = (elemClass.properties[baseAttribute] || {}).type;
|
|
124
|
+
const val = this._payloadCast(type, this.getProperty(msg, this.messageProperty));
|
|
125
|
+
if (type === Boolean && !val) {
|
|
126
|
+
this.removeAttribute(baseAttribute);
|
|
127
|
+
} else {
|
|
128
|
+
this.setAttribute(baseAttribute, val);
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Reserved runtime-control channel — distinct, exact topics so device
|
|
134
|
+
// telemetry sharing the base topic can never reach the element.
|
|
135
|
+
// Consistent with feezal-view / feezal-site addclass / removeclass.
|
|
136
|
+
this._subscribeControl(base);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Subscribe to the reserved <base>/{setattribute,removeattribute,setstyle,removestyle,addclass,removeclass} control topics. */
|
|
140
|
+
_subscribeControl(base) {
|
|
141
|
+
const sub = (suffix, cb) => this._subscriptions.push(feezal.connection.sub(base + '/' + suffix, cb));
|
|
142
|
+
const val = msg => this.getProperty(msg, this.messageProperty);
|
|
143
|
+
const list = p => (Array.isArray(p) ? p : String(p).split(/[,\s]+/)).filter(Boolean);
|
|
144
|
+
|
|
145
|
+
sub('setattribute', msg => {
|
|
146
|
+
const obj = val(msg);
|
|
147
|
+
if (obj && typeof obj === 'object') {
|
|
148
|
+
for (const [k, v] of Object.entries(obj)) this.setAttribute(k, String(v));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
sub('removeattribute', msg => list(val(msg)).forEach(n => this.removeAttribute(n)));
|
|
152
|
+
sub('setstyle', msg => {
|
|
153
|
+
const obj = val(msg);
|
|
154
|
+
if (obj && typeof obj === 'object') Object.assign(this.style, obj);
|
|
155
|
+
});
|
|
156
|
+
sub('removestyle', msg => list(val(msg)).forEach(n => this.style.removeProperty(n)));
|
|
157
|
+
sub('addclass', msg => this.classList.add(val(msg)));
|
|
158
|
+
sub('removeclass', msg => this.classList.remove(val(msg)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_unsubscribe() {
|
|
162
|
+
const sub = this._subscriptions.shift();
|
|
163
|
+
if (sub) {
|
|
164
|
+
feezal.connection.unsubscribe(sub);
|
|
165
|
+
this._unsubscribe();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
constructor() {
|
|
170
|
+
super();
|
|
171
|
+
this._subscriptions = [];
|
|
172
|
+
this._conditions = new FeezalConditions(this);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
connectedCallback() {
|
|
176
|
+
super.connectedCallback();
|
|
177
|
+
this.classList.add('feezal-element');
|
|
178
|
+
// Prevent tab-focus on shadow DOM controls while in the editor.
|
|
179
|
+
if (feezal.isEditor && this.shadowRoot) {
|
|
180
|
+
this.shadowRoot.querySelectorAll('*').forEach(el => {
|
|
181
|
+
if (el.hasAttribute('tabindex')) {
|
|
182
|
+
el.addEventListener('focus', event => {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
if (event.relatedTarget) {
|
|
185
|
+
event.relatedTarget.focus();
|
|
186
|
+
} else {
|
|
187
|
+
event.currentTarget.blur();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
el.setAttribute('tabindex', '-1');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (this.visible || !this.dynamicSubscriptions) {
|
|
195
|
+
this._subscribe();
|
|
196
|
+
this._conditions.connect();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
disconnectedCallback() {
|
|
200
|
+
super.disconnectedCallback();
|
|
201
|
+
this._unsubscribe();
|
|
202
|
+
this._conditions.disconnect();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_visibleChanged(visible) {
|
|
206
|
+
if (!this.dynamicSubscriptions) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (visible) {
|
|
210
|
+
this._subscribe();
|
|
211
|
+
this._conditions.connect();
|
|
212
|
+
} else {
|
|
213
|
+
this._unsubscribe();
|
|
214
|
+
this._conditions.disconnect();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// E50: conditions added/edited/removed at runtime → restart the engine
|
|
219
|
+
// (idempotent for an unchanged attribute value).
|
|
220
|
+
_conditionsChanged() {
|
|
221
|
+
if (this.isConnected && (this.visible || !this.dynamicSubscriptions)) {
|
|
222
|
+
this._conditions.connect();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Utils
|
|
227
|
+
throttle(func, limit) {
|
|
228
|
+
let lastFunc;
|
|
229
|
+
let lastRan;
|
|
230
|
+
return function () {
|
|
231
|
+
const context = this;
|
|
232
|
+
const args = arguments;
|
|
233
|
+
|
|
234
|
+
if (!lastRan) {
|
|
235
|
+
func.apply(context, args);
|
|
236
|
+
lastRan = Date.now();
|
|
237
|
+
} else {
|
|
238
|
+
clearTimeout(lastFunc);
|
|
239
|
+
this._inThrottle = true;
|
|
240
|
+
lastFunc = setTimeout(function () {
|
|
241
|
+
if ((Date.now() - lastRan) >= limit) {
|
|
242
|
+
func.apply(context, args);
|
|
243
|
+
lastRan = Date.now();
|
|
244
|
+
this._inThrottle = false;
|
|
245
|
+
}
|
|
246
|
+
}, limit - (Date.now() - lastRan));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @method split
|
|
253
|
+
* Split str by '.' - supports backslash escaped delimiters
|
|
254
|
+
* @param {string} str
|
|
255
|
+
* @returns {Array.<string>}
|
|
256
|
+
*/
|
|
257
|
+
split(str) {
|
|
258
|
+
str = String(str);
|
|
259
|
+
|
|
260
|
+
// Use native split if possible
|
|
261
|
+
if (str.indexOf('\\') === -1) {
|
|
262
|
+
return str.split('.');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
var res = []; // The result array
|
|
266
|
+
var pos = 0; // Starting position of current chunk
|
|
267
|
+
|
|
268
|
+
function chunk(start, end) {
|
|
269
|
+
// Slice, unescape and push onto result array.
|
|
270
|
+
res.push(str.slice(start, end).replace(/\\\\/g, '\\').replace(/\\\./g, '.'));
|
|
271
|
+
// Set starting position of next chunk.
|
|
272
|
+
pos = end + 1;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var esc; // Boolean indicating if a dot is escaped
|
|
276
|
+
var j;
|
|
277
|
+
var i;
|
|
278
|
+
var l = str.length;
|
|
279
|
+
for (i = 0; i < l; i++) {
|
|
280
|
+
if (str[i] === '.') {
|
|
281
|
+
esc = false;
|
|
282
|
+
// Walk over preceding backslashes in reverse direction
|
|
283
|
+
for (j = i - 1; str[j] === '\\'; j--) {
|
|
284
|
+
esc = !esc;
|
|
285
|
+
}
|
|
286
|
+
// Dot is escaped only if preceded by an odd number of backslashes
|
|
287
|
+
if (!esc) {
|
|
288
|
+
chunk(pos, i);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
chunk(pos, i);
|
|
294
|
+
|
|
295
|
+
return res;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @method getProperty
|
|
300
|
+
* get an objects property. supports nested properties through dot-notation, dots may be escaped by backslash
|
|
301
|
+
* @param {Object} obj
|
|
302
|
+
* @param {string} prop
|
|
303
|
+
* @returns {all} the properties value or undefined
|
|
304
|
+
*/
|
|
305
|
+
getProperty(obj, prop) {
|
|
306
|
+
var type = typeof obj;
|
|
307
|
+
if (type !== 'object' && type !== 'function') {
|
|
308
|
+
if (typeof prop === 'undefined') {
|
|
309
|
+
return obj;
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
var arr = this.split(String(prop));
|
|
314
|
+
var res = obj;
|
|
315
|
+
for (let i = 0, l = arr.length; i < l; i++) {
|
|
316
|
+
if (res) {
|
|
317
|
+
res = res[arr[i]];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return res;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
window.customElements.define('feezal-element', FeezalPolymerElement);
|
|
325
|
+
|
|
326
|
+
export {FeezalPolymerElement, html};
|
package/package.json
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"author": {
|
|
3
|
+
"name": "Sebastian Raff",
|
|
4
|
+
"email": "hobbyquaker@gmail.com"
|
|
5
|
+
},
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/feezal/feezal/issues"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@polymer/polymer": "^3.0.0",
|
|
11
|
+
"lit": "^3.2.0"
|
|
12
|
+
},
|
|
13
|
+
"description": "Base classes for feezal dashboard elements — FeezalElement (Lit 3), FeezalPolymerElement (legacy paper elements) and the shared conditions engine",
|
|
14
|
+
"feezal": {},
|
|
15
|
+
"files": [
|
|
16
|
+
"feezal-element.js",
|
|
17
|
+
"feezal-conditions.js",
|
|
18
|
+
"feezal-polymer-element.js",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/feezal/feezal#readme",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"feezal",
|
|
24
|
+
"feezal-element"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"main": "feezal-element.js",
|
|
28
|
+
"name": "@feezal/feezal-element",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/feezal/feezal.git",
|
|
35
|
+
"directory": "www/packages/@feezal/feezal-element"
|
|
36
|
+
},
|
|
37
|
+
"version": "3.0.0"
|
|
28
38
|
}
|