@feezal/feezal-element 0.8.0 → 3.0.1
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 +173 -185
- package/feezal-polymer-element.js +326 -0
- package/feezal-topic-input.js +173 -0
- package/package.json +37 -25
- package/dist/sw.js +0 -2
- package/dist/sw.js.map +0 -1
- package/dist/workbox-bacb9a40.js +0 -2
- package/dist/workbox-bacb9a40.js.map +0 -1
package/feezal-element.js
CHANGED
|
@@ -1,122 +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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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) * {
|
|
22
|
-
pointer-events: none;
|
|
23
|
-
}
|
|
24
|
-
:host(.feezal-editable.feezal-selected) {
|
|
25
|
-
outline: 2px dashed rgba(250, 120, 0, 0.8);
|
|
26
|
-
}
|
|
27
|
-
</style>
|
|
28
|
-
</template>`;
|
|
46
|
+
static get feezal() {
|
|
47
|
+
return {attributes: [], styles: []};
|
|
48
|
+
}
|
|
29
49
|
|
|
30
|
-
|
|
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
|
+
};
|
|
31
64
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'selected': {
|
|
41
|
-
type: Boolean,
|
|
42
|
-
value: false
|
|
43
|
-
},
|
|
44
|
-
subscribeTopic: {
|
|
45
|
-
type: String,
|
|
46
|
-
value: '',
|
|
47
|
-
reflectToAttribute: true,
|
|
48
|
-
},
|
|
49
|
-
messageProperty: {
|
|
50
|
-
type: String,
|
|
51
|
-
value: 'payload',
|
|
52
|
-
reflectToAttribute: true,
|
|
53
|
-
},
|
|
54
|
-
'dynamicSubscriptions': {
|
|
55
|
-
type: Boolean,
|
|
56
|
-
value: false,
|
|
57
|
-
reflectToAttribute: true
|
|
58
|
-
},
|
|
59
|
-
'visible': {
|
|
60
|
-
type: Boolean,
|
|
61
|
-
value: false,
|
|
62
|
-
observer: '_visibleChanged'
|
|
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);
|
|
65
73
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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();
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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();
|
|
80
98
|
}
|
|
81
|
-
}
|
|
82
|
-
|
|
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();
|
|
83
106
|
}
|
|
84
107
|
}
|
|
85
108
|
|
|
86
|
-
|
|
87
|
-
|
|
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));
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
_subscribe() {
|
|
91
|
-
|
|
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) {
|
|
92
123
|
return;
|
|
93
124
|
}
|
|
94
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;
|
|
95
137
|
const elemClass = window.customElements.get(this.localName);
|
|
96
138
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|
|
104
152
|
|
|
105
|
-
|
|
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
|
+
}
|
|
106
158
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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));
|
|
115
169
|
}
|
|
116
|
-
})
|
|
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)));
|
|
117
179
|
}
|
|
118
180
|
|
|
119
181
|
_unsubscribe() {
|
|
182
|
+
this._subscribed = false;
|
|
120
183
|
const sub = this._subscriptions.shift();
|
|
121
184
|
if (sub) {
|
|
122
185
|
feezal.connection.unsubscribe(sub);
|
|
@@ -124,146 +187,72 @@ class FeezalElement extends PolymerElement {
|
|
|
124
187
|
}
|
|
125
188
|
}
|
|
126
189
|
|
|
127
|
-
|
|
128
|
-
super();
|
|
129
|
-
this._subscriptions = [];
|
|
130
|
-
}
|
|
190
|
+
// ── Utilities (ported 1:1 from the Polymer base) ─────────────────────────
|
|
131
191
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (feezal.isEditor) {
|
|
136
|
-
/*
|
|
137
|
-
if (!this.shadowRoot.querySelector('.feezal-blocker')) {
|
|
138
|
-
const el = document.createElement('div');
|
|
139
|
-
el.className = 'feezal-blocker';
|
|
140
|
-
this.shadowRoot.prepend(el);
|
|
141
|
-
}
|
|
142
|
-
*/
|
|
143
|
-
this.shadowRoot.querySelectorAll('*').forEach(el => {
|
|
144
|
-
if (el.hasAttribute('tabindex')) {
|
|
145
|
-
el.addEventListener('focus', event => {
|
|
146
|
-
event.preventDefault();
|
|
147
|
-
if (event.relatedTarget) {
|
|
148
|
-
event.relatedTarget.focus();
|
|
149
|
-
} else {
|
|
150
|
-
event.currentTarget.blur();
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
el.setAttribute('tabindex', '-1');
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
} else {
|
|
157
|
-
if (this.visible || !this.dynamicSubscriptions) {
|
|
158
|
-
this._subscribe();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
disconnectedCallback() {
|
|
163
|
-
super.disconnectedCallback();
|
|
164
|
-
this._unsubscribe();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_visibleChanged(visible) {
|
|
168
|
-
if (feezal.isEditor || !this.dynamicSubscriptions) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (visible) {
|
|
172
|
-
this._subscribe();
|
|
173
|
-
} else {
|
|
174
|
-
this._unsubscribe();
|
|
192
|
+
_payloadCast(type, payload) {
|
|
193
|
+
if (typeof payload === 'string' && type === Boolean) {
|
|
194
|
+
return Number(payload) !== 0 && payload.toLowerCase() !== 'false';
|
|
175
195
|
}
|
|
196
|
+
return payload;
|
|
176
197
|
}
|
|
177
198
|
|
|
178
|
-
// Utils
|
|
179
199
|
throttle(func, limit) {
|
|
180
200
|
let lastFunc;
|
|
181
201
|
let lastRan;
|
|
182
202
|
return function () {
|
|
183
203
|
const context = this;
|
|
184
|
-
const args
|
|
185
|
-
|
|
204
|
+
const args = arguments;
|
|
186
205
|
if (!lastRan) {
|
|
187
206
|
func.apply(context, args);
|
|
188
207
|
lastRan = Date.now();
|
|
189
208
|
} else {
|
|
190
209
|
clearTimeout(lastFunc);
|
|
191
|
-
this._inThrottle = true;
|
|
192
210
|
lastFunc = setTimeout(function () {
|
|
193
211
|
if ((Date.now() - lastRan) >= limit) {
|
|
194
212
|
func.apply(context, args);
|
|
195
213
|
lastRan = Date.now();
|
|
196
|
-
this._inThrottle = false;
|
|
197
214
|
}
|
|
198
215
|
}, limit - (Date.now() - lastRan));
|
|
199
216
|
}
|
|
200
|
-
}
|
|
217
|
+
};
|
|
201
218
|
}
|
|
202
219
|
|
|
203
|
-
/**
|
|
204
|
-
* @method split
|
|
205
|
-
* Split str by '.' - supports backslash escaped delimiters
|
|
206
|
-
* @param {string} str
|
|
207
|
-
* @returns {Array.<string>}
|
|
208
|
-
*/
|
|
209
220
|
split(str) {
|
|
210
221
|
str = String(str);
|
|
211
|
-
|
|
212
|
-
// Use native split if possible
|
|
213
222
|
if (str.indexOf('\\') === -1) {
|
|
214
223
|
return str.split('.');
|
|
215
224
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
var pos = 0; // Starting position of current chunk
|
|
219
|
-
|
|
225
|
+
const res = [];
|
|
226
|
+
let pos = 0;
|
|
220
227
|
function chunk(start, end) {
|
|
221
|
-
// Slice, unescape and push onto result array.
|
|
222
228
|
res.push(str.slice(start, end).replace(/\\\\/g, '\\').replace(/\\\./g, '.'));
|
|
223
|
-
// Set starting position of next chunk.
|
|
224
229
|
pos = end + 1;
|
|
225
230
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
var i;
|
|
230
|
-
var l = str.length;
|
|
231
|
+
let esc, j;
|
|
232
|
+
const l = str.length;
|
|
233
|
+
let i;
|
|
231
234
|
for (i = 0; i < l; i++) {
|
|
232
235
|
if (str[i] === '.') {
|
|
233
236
|
esc = false;
|
|
234
|
-
// Walk over preceding backslashes in reverse direction
|
|
235
237
|
for (j = i - 1; str[j] === '\\'; j--) {
|
|
236
238
|
esc = !esc;
|
|
237
239
|
}
|
|
238
|
-
// Dot is escaped only if preceded by an odd number of backslashes
|
|
239
240
|
if (!esc) {
|
|
240
241
|
chunk(pos, i);
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
|
-
|
|
245
245
|
chunk(pos, i);
|
|
246
|
-
|
|
247
246
|
return res;
|
|
248
247
|
}
|
|
249
248
|
|
|
250
|
-
/**
|
|
251
|
-
* @method getProperty
|
|
252
|
-
* get an objects property. supports nested properties through dot-notation, dots may be escaped by backslash
|
|
253
|
-
* @param {Object} obj
|
|
254
|
-
* @param {string} prop
|
|
255
|
-
* @returns {all} the properties value or undefined
|
|
256
|
-
*/
|
|
257
249
|
getProperty(obj, prop) {
|
|
258
|
-
|
|
250
|
+
const type = typeof obj;
|
|
259
251
|
if (type !== 'object' && type !== 'function') {
|
|
260
|
-
|
|
261
|
-
return obj;
|
|
262
|
-
}
|
|
263
|
-
return undefined;
|
|
252
|
+
return typeof prop === 'undefined' ? obj : undefined;
|
|
264
253
|
}
|
|
265
|
-
|
|
266
|
-
|
|
254
|
+
const arr = this.split(String(prop));
|
|
255
|
+
let res = obj;
|
|
267
256
|
for (let i = 0, l = arr.length; i < l; i++) {
|
|
268
257
|
if (res) {
|
|
269
258
|
res = res[arr[i]];
|
|
@@ -273,6 +262,5 @@ class FeezalElement extends PolymerElement {
|
|
|
273
262
|
}
|
|
274
263
|
}
|
|
275
264
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
export {FeezalElement, html};
|
|
265
|
+
// Re-export so element files can do a single import.
|
|
266
|
+
export {html, css};
|