@feezal/feezal-element 3.0.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.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * feezal-topic-input — shared MQTT-topic input with server-backed
3
+ * autocompletion (B28).
4
+ *
5
+ * Drop-in replacement for the plain `sl-input` topic fields in custom (N6)
6
+ * inspectors: wraps an sl-input and adds the same completion dropdown as the
7
+ * generic attribute inspector (debounced fetch of /api/topics/completions,
8
+ * arrow-key navigation, Enter to pick, descend on `topic/` intermediate
9
+ * completions, Escape/Tab to dismiss).
10
+ *
11
+ * The inner sl-input's `sl-input`/`sl-change` events bubble composed out of
12
+ * the shadow root retargeted to this element, and `value` is kept in sync —
13
+ * existing inspector handlers keep working unchanged with
14
+ * `@sl-change="${e => ...e.target.value...}"`. Picking a completion fires a
15
+ * synthetic `sl-change`; descending an intermediate `…/` completion fires
16
+ * `sl-input` only.
17
+ *
18
+ * Only usable inside the editor (needs the Shoelace bundle and the feezal
19
+ * server API); dashboards never render it.
20
+ */
21
+ import {LitElement, html, css} from 'lit';
22
+
23
+ class FeezalTopicInput extends LitElement {
24
+ static properties = {
25
+ label: {type: String},
26
+ value: {type: String},
27
+ placeholder: {type: String},
28
+ size: {type: String},
29
+ _completions: {state: true},
30
+ _cursor: {state: true},
31
+ };
32
+
33
+ static styles = css`
34
+ :host { display: block; }
35
+ .topic-wrap { position: relative; }
36
+ sl-input { width: 100%; }
37
+ sl-input::part(form-control-label) { color: var(--sl-input-label-color, inherit); font-size: 12px; }
38
+ sl-input::part(base) {
39
+ background: var(--feezal-bg, #fff);
40
+ border-color: var(--feezal-border, #ccc);
41
+ color: var(--feezal-color, #333);
42
+ }
43
+ sl-input::part(input) { background: var(--feezal-bg, #fff); color: var(--sl-input-color, #333); }
44
+ .completions {
45
+ position: absolute; top: 100%; left: 0; right: 0; z-index: 500;
46
+ list-style: none; margin: 2px 0 0; padding: 4px 0;
47
+ background: var(--feezal-bg, #fff); border: 1px solid var(--feezal-border, #ccc);
48
+ border-radius: 4px; box-shadow: 0 4px 14px rgba(0,0,0,0.18);
49
+ max-height: 180px; overflow-y: auto; font-size: 12px;
50
+ }
51
+ .completions li {
52
+ padding: 4px 10px; cursor: pointer;
53
+ color: var(--feezal-color, #333);
54
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
55
+ }
56
+ .completions li:hover, .completions li.active { background: var(--feezal-bg-sub, #f0f0f0); }
57
+ `;
58
+
59
+ constructor() {
60
+ super();
61
+ this.label = '';
62
+ this.value = '';
63
+ this.placeholder = 'mqtt/topic';
64
+ this.size = 'small';
65
+ this._completions = [];
66
+ this._cursor = -1;
67
+ }
68
+
69
+ disconnectedCallback() {
70
+ super.disconnectedCallback();
71
+ clearTimeout(this._fetchTimer);
72
+ clearTimeout(this._closeTimer);
73
+ }
74
+
75
+ _emit(name) {
76
+ this.dispatchEvent(new CustomEvent(name, {bubbles: true, composed: true}));
77
+ }
78
+
79
+ _debounceFetch(prefix) {
80
+ clearTimeout(this._fetchTimer);
81
+ this._fetchTimer = setTimeout(() => this._fetchCompletions(prefix), 150);
82
+ }
83
+
84
+ async _fetchCompletions(prefix) {
85
+ try {
86
+ const r = await fetch(`/api/topics/completions?prefix=${encodeURIComponent(prefix)}`);
87
+ if (!r.ok) { this._completions = []; return; }
88
+ const {completions} = await r.json();
89
+ this._cursor = -1;
90
+ this._completions = completions || [];
91
+ } catch {
92
+ this._completions = [];
93
+ }
94
+ }
95
+
96
+ _close() {
97
+ this._completions = [];
98
+ this._cursor = -1;
99
+ }
100
+
101
+ _onInput(e) {
102
+ this.value = e.target.value;
103
+ this._debounceFetch(this.value);
104
+ }
105
+
106
+ _onKeydown(e) {
107
+ if (!this._completions.length) return;
108
+ if (e.key === 'ArrowDown') {
109
+ e.preventDefault();
110
+ this._cursor = Math.min(this._cursor + 1, this._completions.length - 1);
111
+ } else if (e.key === 'ArrowUp') {
112
+ e.preventDefault();
113
+ this._cursor = Math.max(this._cursor - 1, -1);
114
+ } else if (e.key === 'Enter' && this._cursor >= 0) {
115
+ e.preventDefault();
116
+ e.stopPropagation();
117
+ this._select(this._completions[this._cursor]);
118
+ } else if (e.key === 'Escape' || e.key === 'Tab') {
119
+ this._close();
120
+ }
121
+ }
122
+
123
+ _select(val) {
124
+ this.value = val;
125
+ if (val.endsWith('/')) {
126
+ // Intermediate level — descend and keep the dropdown open.
127
+ this._emit('sl-input');
128
+ this._fetchCompletions(val);
129
+ } else {
130
+ this._emit('sl-input');
131
+ this._emit('sl-change');
132
+ this._close();
133
+ }
134
+ }
135
+
136
+ _scheduleClose() {
137
+ clearTimeout(this._closeTimer);
138
+ this._closeTimer = setTimeout(() => this._close(), 200);
139
+ }
140
+
141
+ render() {
142
+ return html`
143
+ <div class="topic-wrap">
144
+ <sl-input size="${this.size}" autocomplete="off"
145
+ label="${this.label || ''}"
146
+ placeholder="${this.placeholder}"
147
+ .value="${this.value ?? ''}"
148
+ @sl-focus="${() => this._debounceFetch(this.value ?? '')}"
149
+ @sl-input="${this._onInput}"
150
+ @sl-change="${e => { this.value = e.target.value; }}"
151
+ @sl-blur="${this._scheduleClose}"
152
+ @keydown="${this._onKeydown}"></sl-input>
153
+ ${this._completions.length ? html`
154
+ <ul class="completions">
155
+ ${this._completions.map((c, ci) => html`
156
+ <li class="${ci === this._cursor ? 'active' : ''}"
157
+ @mousedown="${e => { e.preventDefault(); this._select(c); }}">
158
+ ${c}
159
+ </li>
160
+ `)}
161
+ </ul>
162
+ ` : ''}
163
+ </div>
164
+ `;
165
+ }
166
+ }
167
+
168
+ // Multiple element bundles may carry a copy (static exports) — guard the define.
169
+ if (!customElements.get('feezal-topic-input')) {
170
+ customElements.define('feezal-topic-input', FeezalTopicInput);
171
+ }
172
+
173
+ export {FeezalTopicInput};
package/package.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "feezal-element.js",
17
17
  "feezal-conditions.js",
18
18
  "feezal-polymer-element.js",
19
+ "feezal-topic-input.js",
19
20
  "LICENSE"
20
21
  ],
21
22
  "homepage": "https://github.com/feezal/feezal#readme",
@@ -34,5 +35,5 @@
34
35
  "url": "git+https://github.com/feezal/feezal.git",
35
36
  "directory": "www/packages/@feezal/feezal-element"
36
37
  },
37
- "version": "3.0.0"
38
+ "version": "3.0.1"
38
39
  }