@tillsc/progressive-web-components 0.1.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.
@@ -0,0 +1,368 @@
1
+ // src/core/pwc-element.js
2
+ var PwcElement = class extends HTMLElement {
3
+ /**
4
+ * List of DOM event types to bind on the host element.
5
+ * Subclasses may override.
6
+ *
7
+ * Example:
8
+ * static events = ["click", "input"];
9
+ */
10
+ static events = [];
11
+ static registerCss(cssText) {
12
+ const sheet = new CSSStyleSheet();
13
+ sheet.replaceSync(cssText);
14
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
15
+ }
16
+ connectedCallback() {
17
+ if (this._connected) return;
18
+ this._connected = true;
19
+ this._bindEvents();
20
+ }
21
+ disconnectedCallback() {
22
+ if (!this._connected) return;
23
+ this._connected = false;
24
+ this._unbindEvents();
25
+ this.onDisconnect();
26
+ }
27
+ /**
28
+ * Optional cleanup hook for subclasses.
29
+ */
30
+ onDisconnect() {
31
+ }
32
+ /**
33
+ * Bind declared events using the handleEvent pattern.
34
+ */
35
+ _bindEvents() {
36
+ const events = this.constructor.events ?? [];
37
+ for (const type of events) {
38
+ this.addEventListener(type, this);
39
+ }
40
+ }
41
+ /**
42
+ * Unbind all previously declared events.
43
+ */
44
+ _unbindEvents() {
45
+ const events = this.constructor.events ?? [];
46
+ for (const type of events) {
47
+ this.removeEventListener(type, this);
48
+ }
49
+ }
50
+ /**
51
+ * Default event handler.
52
+ * Subclasses are expected to override this method
53
+ * and route events as needed.
54
+ */
55
+ handleEvent(_event) {
56
+ }
57
+ };
58
+
59
+ // src/core/pwc-children-observer-element.js
60
+ var PwcChildrenObserverElement = class extends PwcElement {
61
+ static observeMode = "children";
62
+ // "children" | "tree"
63
+ connectedCallback() {
64
+ if (this._connected) return;
65
+ super.connectedCallback();
66
+ this._startChildrenObserver();
67
+ }
68
+ disconnectedCallback() {
69
+ this._stopChildrenObserver();
70
+ super.disconnectedCallback();
71
+ }
72
+ onChildrenChanged(_mutations) {
73
+ }
74
+ /** Run fn() without triggering onChildrenChanged for the resulting DOM mutations. */
75
+ _withoutChildrenChangedNotification(fn) {
76
+ fn();
77
+ this._childrenObserver?.takeRecords();
78
+ }
79
+ _startChildrenObserver() {
80
+ const mode = this.constructor.observeMode || "children";
81
+ const subtree = mode === "tree";
82
+ this._childrenObserver = new MutationObserver((mutations) => {
83
+ if (!this._connected) return;
84
+ this.onChildrenChanged(mutations);
85
+ });
86
+ this._childrenObserver.observe(this, { childList: true, subtree });
87
+ this.onChildrenChanged([]);
88
+ }
89
+ _stopChildrenObserver() {
90
+ if (!this._childrenObserver) return;
91
+ this._childrenObserver.disconnect();
92
+ this._childrenObserver = null;
93
+ }
94
+ };
95
+
96
+ // src/multiselect-dual-list/base.js
97
+ var MultiselectDualListBase = class extends PwcChildrenObserverElement {
98
+ static observeMode = "tree";
99
+ static events = ["click", "input"];
100
+ get _selectedClass() {
101
+ return "pwc-msdl-item--selected";
102
+ }
103
+ onChildrenChanged() {
104
+ const select = this.querySelector("select");
105
+ if (!select) return;
106
+ this._select = select;
107
+ const items = this._parseOptions(select);
108
+ this._items = items;
109
+ this._itemsByValue = new Map(items.map((item) => [item.value, item]));
110
+ this._withoutChildrenChangedNotification(() => {
111
+ if (!this._availableList) {
112
+ const ui = this._buildUI();
113
+ this._availableList = ui.availableList;
114
+ this._selectedList = ui.selectedList;
115
+ this._filterInput = ui.filterInput;
116
+ }
117
+ this._populateLists(items);
118
+ select.style.display = "none";
119
+ const filterText = this._filterInput?.value;
120
+ if (filterText) this._applyFilter(filterText);
121
+ });
122
+ }
123
+ _populateLists(items) {
124
+ this._availableList.replaceChildren();
125
+ this._selectedList.replaceChildren();
126
+ for (const item of items) {
127
+ this._availableList.appendChild(this._createAvailableEntry(item));
128
+ }
129
+ for (const item of items) {
130
+ if (item.selected) {
131
+ this._selectedList.appendChild(this._createSelectedEntry(item));
132
+ }
133
+ }
134
+ }
135
+ _parseOptions(select) {
136
+ const options = Array.from(select.options);
137
+ const parentMap = /* @__PURE__ */ new Map();
138
+ for (const opt of options) {
139
+ const parent = opt.dataset.parent;
140
+ if (parent) parentMap.set(opt.value, parent);
141
+ }
142
+ return options.map((opt) => ({
143
+ value: opt.value,
144
+ label: opt.textContent,
145
+ parent: opt.dataset.parent || null,
146
+ depth: this._calculateDepth(opt.value, parentMap),
147
+ selected: opt.selected,
148
+ disabled: opt.disabled,
149
+ warnOnUnselect: opt.dataset.warnOnUnselect || null
150
+ }));
151
+ }
152
+ _calculateDepth(value, parentMap) {
153
+ let depth = 0;
154
+ let current = value;
155
+ const visited = /* @__PURE__ */ new Set();
156
+ while (parentMap.has(current)) {
157
+ if (visited.has(current)) break;
158
+ visited.add(current);
159
+ current = parentMap.get(current);
160
+ depth++;
161
+ }
162
+ return depth;
163
+ }
164
+ handleEvent(e) {
165
+ if (e.type === "click") {
166
+ const actionEl = e.target.closest("[data-action]");
167
+ if (!actionEl || !this.contains(actionEl)) return;
168
+ const action = actionEl.dataset.action;
169
+ const value = actionEl.closest("[data-value]")?.dataset.value;
170
+ if (!value) return;
171
+ if (action === "add") this._addItem(value);
172
+ else if (action === "remove") this._removeItem(value);
173
+ return;
174
+ }
175
+ if (e.type === "input") {
176
+ if (this._filterInput && e.target === this._filterInput) {
177
+ this._applyFilter(this._filterInput.value);
178
+ }
179
+ }
180
+ }
181
+ _addItem(value) {
182
+ const item = this._itemsByValue.get(value);
183
+ if (!item || item.disabled) return;
184
+ if (!this.select.hasAttribute("multiple")) {
185
+ for (const opt2 of this._select.options) {
186
+ if (opt2.selected) opt2.selected = false;
187
+ }
188
+ for (const el of this._availableList.querySelectorAll(`.${this._selectedClass}`)) {
189
+ el.classList.remove(this._selectedClass);
190
+ el.setAttribute("aria-selected", "false");
191
+ const btn = el.querySelector("[data-action='add']");
192
+ if (btn) btn.style.display = "";
193
+ }
194
+ }
195
+ const opt = this._select.querySelector(`option[value="${CSS.escape(value)}"]`);
196
+ if (opt) opt.selected = true;
197
+ const availEl = this._availableList.querySelector(`[data-value="${CSS.escape(value)}"]`);
198
+ if (availEl) {
199
+ availEl.classList.add(this._selectedClass);
200
+ availEl.setAttribute("aria-selected", "true");
201
+ const btn = availEl.querySelector("[data-action='add']");
202
+ if (btn) btn.style.display = "none";
203
+ }
204
+ this._withoutChildrenChangedNotification(() => {
205
+ if (!this.select.hasAttribute("multiple")) this._selectedList.replaceChildren();
206
+ this._selectedList.appendChild(this._createSelectedEntry(item));
207
+ });
208
+ }
209
+ _removeItem(value) {
210
+ const item = this._itemsByValue.get(value);
211
+ if (!item) return;
212
+ if (item.warnOnUnselect && !confirm(item.warnOnUnselect)) return;
213
+ const opt = this._select.querySelector(`option[value="${CSS.escape(value)}"]`);
214
+ if (opt) opt.selected = false;
215
+ const availEl = this._availableList.querySelector(`[data-value="${CSS.escape(value)}"]`);
216
+ if (availEl) {
217
+ availEl.classList.remove(this._selectedClass);
218
+ availEl.setAttribute("aria-selected", "false");
219
+ const btn = availEl.querySelector("[data-action='add']");
220
+ if (btn) btn.style.display = "";
221
+ }
222
+ const selEl = this._selectedList.querySelector(`[data-value="${CSS.escape(value)}"]`);
223
+ if (selEl) this._withoutChildrenChangedNotification(() => selEl.remove());
224
+ }
225
+ get select() {
226
+ return this._select;
227
+ }
228
+ get selectedLabel() {
229
+ return this.getAttribute("selected-label") || "Selected";
230
+ }
231
+ get availableLabel() {
232
+ return this.getAttribute("available-label") || "Available";
233
+ }
234
+ get addLabel() {
235
+ return this.getAttribute("add-label") || "\u2190";
236
+ }
237
+ get removeLabel() {
238
+ return this.getAttribute("remove-label") || "\xD7";
239
+ }
240
+ get filterText() {
241
+ return this._filterInput?.value ?? "";
242
+ }
243
+ set filterText(text) {
244
+ if (this._filterInput) this._filterInput.value = text;
245
+ this._applyFilter(text);
246
+ }
247
+ _applyFilter(text) {
248
+ const { matchCount, totalCount } = this._filterAvailable(text);
249
+ this.dispatchEvent(new CustomEvent("pwc-multiselect-dual-list:filter", {
250
+ bubbles: true,
251
+ detail: { filterText: text, matchCount, totalCount }
252
+ }));
253
+ }
254
+ _buildFilterRegex(text) {
255
+ if (!text) return null;
256
+ try {
257
+ return new RegExp(text, "i");
258
+ } catch {
259
+ return new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
260
+ }
261
+ }
262
+ };
263
+
264
+ // src/core/utils.js
265
+ function defineOnce(name, classDef) {
266
+ if (customElements.get(name)) return;
267
+ customElements.define(name, classDef);
268
+ }
269
+
270
+ // src/multiselect-dual-list/bs5/multiselect-dual-list.js
271
+ var PwcMultiselectDualListBs5 = class extends MultiselectDualListBase {
272
+ get _selectedClass() {
273
+ return "list-group-item-secondary";
274
+ }
275
+ _buildUI() {
276
+ const container = document.createElement("div");
277
+ container.innerHTML = `
278
+ <div class="col">
279
+ <h6>${this.selectedLabel}</h6>
280
+ <div class="list-group" style="max-height:20em;overflow-y:auto" role="listbox" aria-label="${this.selectedLabel}" data-role="selected"></div>
281
+ </div>
282
+ <div class="col">
283
+ <h6>${this.availableLabel}</h6>
284
+ <input type="search" class="form-control form-control-sm mb-2" placeholder="Filter\u2026" aria-label="Filter ${this.availableLabel}" />
285
+ <div class="list-group" style="max-height:20em;overflow-y:auto" role="listbox" aria-label="${this.availableLabel}" data-role="available"></div>
286
+ </div>
287
+ `;
288
+ container.className = "row g-3";
289
+ this.select.after(container);
290
+ return {
291
+ selectedList: container.querySelector("[data-role='selected']"),
292
+ availableList: container.querySelector("[data-role='available']"),
293
+ filterInput: container.querySelector("input[type='search']")
294
+ };
295
+ }
296
+ _createEntry(item) {
297
+ const el = document.createElement("div");
298
+ el.className = "list-group-item d-flex justify-content-between align-items-center";
299
+ el.role = "option";
300
+ el.dataset.value = item.value;
301
+ const label = document.createElement("span");
302
+ label.textContent = item.label;
303
+ el.appendChild(label);
304
+ return el;
305
+ }
306
+ _createAvailableEntry(item) {
307
+ const el = this._createEntry(item);
308
+ el.setAttribute("aria-selected", String(item.selected));
309
+ if (item.disabled) {
310
+ el.classList.add("disabled");
311
+ el.setAttribute("aria-disabled", "true");
312
+ }
313
+ if (item.selected) el.classList.add("list-group-item-secondary");
314
+ if (item.depth > 0) el.style.paddingLeft = `${item.depth * 1.5 + 0.75}em`;
315
+ if (!item.disabled) {
316
+ const btn = document.createElement("button");
317
+ btn.type = "button";
318
+ btn.className = "btn btn-sm btn-outline-primary";
319
+ btn.dataset.action = "add";
320
+ btn.textContent = this.addLabel;
321
+ btn.setAttribute("aria-label", `${this.addLabel} ${item.label}`);
322
+ if (item.selected) btn.style.display = "none";
323
+ el.appendChild(btn);
324
+ }
325
+ return el;
326
+ }
327
+ _createSelectedEntry(item) {
328
+ const el = this._createEntry(item);
329
+ const btn = document.createElement("button");
330
+ btn.type = "button";
331
+ btn.className = "btn btn-sm btn-outline-danger";
332
+ btn.dataset.action = "remove";
333
+ btn.textContent = this.removeLabel;
334
+ btn.setAttribute("aria-label", `${this.removeLabel} ${item.label}`);
335
+ el.appendChild(btn);
336
+ return el;
337
+ }
338
+ _filterAvailable(text) {
339
+ const items = this._availableList.querySelectorAll("[data-value]");
340
+ const totalCount = items.length;
341
+ const regex = this._buildFilterRegex(text);
342
+ if (!regex) {
343
+ for (const el of items) el.classList.remove("d-none");
344
+ return { matchCount: totalCount, totalCount };
345
+ }
346
+ let matchCount = 0;
347
+ for (const el of items) {
348
+ const label = el.querySelector("span")?.textContent || "";
349
+ const match = regex.test(label);
350
+ el.classList.toggle("d-none", !match);
351
+ if (match) matchCount++;
352
+ }
353
+ return { matchCount, totalCount };
354
+ }
355
+ };
356
+ var define = () => defineOnce("pwc-multiselect-dual-list-bs5", PwcMultiselectDualListBs5);
357
+
358
+ // src/multiselect-dual-list/bs5/index.js
359
+ function register() {
360
+ PwcMultiselectDualListBs5.registerCss(
361
+ "pwc-multiselect-dual-list-bs5[hide-selected] .list-group-item-secondary { display: none; }"
362
+ );
363
+ define();
364
+ }
365
+ register();
366
+ export {
367
+ register
368
+ };