composite-select 1.0.3 → 1.0.4

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.
@@ -1,245 +0,0 @@
1
- import { OptionsSectionManager } from "./OptionsSectionManager.js";
2
- /**
3
- * Injects CSS into the Shadow DOM.
4
- * Priority:
5
- * 1. OptionsSection.cssText (Bundler string injection)
6
- * 2. <meta name="select-component" content="/path1.css, /path2.css"> (Global HTML declaration in main document)
7
- * 3. OptionsSection.defaultCssUrls (Global JS property)
8
- *
9
- * Example of Global HTML Declaration in the main document <head>:
10
- * <head>
11
- * <meta name="select-component" content="OptionsSectionManager.css" />
12
- * </head>
13
- */
14
- export class OptionsSection extends HTMLElement {
15
- /**
16
- * Bundlers can inject raw CSS string here to avoid HTTP requests entirely.
17
- * e.g., OptionsSection.cssText = import('./OptionsSectionManager.css?raw');
18
- */
19
- static cssText = "";
20
- static defaultCssUrls = [];
21
- _manager = null;
22
- _options = {};
23
- _stylesInjected = false;
24
- _mountPoint;
25
- static get observedAttributes() {
26
- return ["options", "loading", "value", "label", "disabled", "max-height", "show-footer", "show-filter"];
27
- }
28
- constructor() {
29
- super();
30
- this.attachShadow({ mode: "open" });
31
- this.shadowRoot.innerHTML = `<style></style><div></div>`;
32
- this._mountPoint = this.shadowRoot.querySelector("div");
33
- }
34
- connectedCallback() {
35
- this._injectStyles();
36
- if (this._manager)
37
- return;
38
- this._options = {
39
- options: this._parseJSON(this.getAttribute("options")) ?? [],
40
- loading: this.hasAttribute("loading"),
41
- value: this.getAttribute("value") || "",
42
- label: this.getAttribute("label") || "",
43
- disabled: this.hasAttribute("disabled"),
44
- maxHeight: this.getAttribute("max-height") || "",
45
- showFooter: this.hasAttribute("show-footer"),
46
- showFilter: this.hasAttribute("show-filter"),
47
- };
48
- this._manager = new OptionsSectionManager(this._mountPoint, this._options);
49
- }
50
- disconnectedCallback() {
51
- this._manager?.destroy();
52
- }
53
- attributeChangedCallback(name, _oldValue, newValue) {
54
- if (!this._manager)
55
- return;
56
- const isTrue = this.hasAttribute(name);
57
- switch (name) {
58
- case "options": {
59
- const parsed = this._parseJSON(newValue);
60
- if (parsed !== undefined) {
61
- this._manager.setOptions(parsed);
62
- }
63
- break;
64
- }
65
- case "loading":
66
- this._manager.setLoading(isTrue);
67
- break;
68
- case "value":
69
- this._manager.setValue(newValue);
70
- break;
71
- case "label":
72
- this._manager.setLabel(newValue);
73
- break;
74
- case "disabled":
75
- this._manager.setDisabled(isTrue);
76
- break;
77
- case "max-height":
78
- this._manager.setMaxHeight(newValue);
79
- break;
80
- case "show-footer":
81
- this._manager.setShowFooter(isTrue);
82
- break;
83
- case "show-filter":
84
- this._manager.setShowFilter(isTrue);
85
- break;
86
- }
87
- }
88
- _injectStyles() {
89
- if (this._stylesInjected)
90
- return;
91
- this._stylesInjected = true;
92
- const style = document.createElement("style");
93
- // Scenario A: Bundler injected raw CSS string directly
94
- if (OptionsSection.cssText) {
95
- style.textContent = OptionsSection.cssText;
96
- }
97
- // Scenario B: Load from URLs (Global Meta Tag > Default Static Property)
98
- else {
99
- let urls = [];
100
- const metaTag = document.querySelector('meta[name="select-component"]');
101
- if (metaTag && metaTag.getAttribute("content")) {
102
- urls = metaTag
103
- .getAttribute("content")
104
- .split(",")
105
- .map((s) => s.trim());
106
- }
107
- else {
108
- urls = OptionsSection.defaultCssUrls;
109
- }
110
- urls.forEach((url) => {
111
- if (!url)
112
- return;
113
- style.textContent += `@import url("${url}");\n`;
114
- });
115
- }
116
- // Remove existing injected CSS if updating dynamically (e.g. css-urls changed)
117
- const existingStyle = this.shadowRoot.querySelector("style");
118
- if (existingStyle) {
119
- existingStyle.remove();
120
- }
121
- this.shadowRoot.appendChild(style);
122
- }
123
- // Proxied methods
124
- setMaxHeight(maxHeight) {
125
- if (maxHeight)
126
- this.setAttribute("max-height", maxHeight);
127
- else
128
- this.removeAttribute("max-height");
129
- }
130
- setDisabled(disabled) {
131
- this.toggleAttribute("disabled", disabled);
132
- }
133
- setShowFooter(show) {
134
- this.toggleAttribute("show-footer", show);
135
- }
136
- setShowFilter(show) {
137
- this.toggleAttribute("show-filter", show);
138
- }
139
- setOptions(options) {
140
- // we don't necessarily want to stringify options back to attribute for performance and avoiding circularity
141
- // but we can if we want to stay in sync. SelectedSection doesn't seem to do it for 'list'.
142
- // this.setAttribute("options", JSON.stringify(options));
143
- this._manager?.setOptions(options);
144
- }
145
- setLoading(loading) {
146
- this.toggleAttribute("loading", loading);
147
- }
148
- setValue(value) {
149
- this.setAttribute("value", value);
150
- }
151
- setLabel(label) {
152
- this.setAttribute("label", label);
153
- }
154
- setRenderEmpty(renderer) {
155
- this._manager?.setRenderEmpty(renderer);
156
- }
157
- setRenderItem(renderer) {
158
- this._manager?.setRenderItem(renderer);
159
- }
160
- setRenderList(renderer) {
161
- this._manager?.setRenderList(renderer);
162
- }
163
- setFocus() {
164
- this._manager?.setFocus();
165
- }
166
- setBlur() {
167
- this._manager?.setBlur();
168
- }
169
- highlightAndScrollToElementOnTheList(id) {
170
- this._manager?.highlightAndScrollToElementOnTheList(id);
171
- }
172
- render() {
173
- this._manager?.render();
174
- }
175
- // Getters and setters
176
- get options() {
177
- return this._manager?.propOptions.options || [];
178
- }
179
- set options(val) {
180
- this.setOptions(val);
181
- }
182
- get loading() {
183
- return this.hasAttribute("loading");
184
- }
185
- set loading(val) {
186
- this.setLoading(val);
187
- }
188
- get value() {
189
- return this._manager?.propInputElement?.value ?? this.getAttribute("value") ?? "";
190
- }
191
- set value(val) {
192
- this.setValue(val);
193
- }
194
- get label() {
195
- return this.getAttribute("label") || "";
196
- }
197
- set label(val) {
198
- this.setLabel(val);
199
- }
200
- get disabled() {
201
- return this.hasAttribute("disabled");
202
- }
203
- set disabled(val) {
204
- this.setDisabled(val);
205
- }
206
- get maxHeight() {
207
- return this.getAttribute("max-height") || "";
208
- }
209
- set maxHeight(val) {
210
- this.setMaxHeight(val);
211
- }
212
- get footer() {
213
- return this.hasAttribute("show-footer") ? this.getAttribute("show-footer") !== "false" : true;
214
- }
215
- set footer(val) {
216
- this.setShowFooter(val);
217
- }
218
- get filter() {
219
- return this.hasAttribute("show-filter") ? this.getAttribute("show-filter") !== "false" : true;
220
- }
221
- set filter(val) {
222
- this.setShowFilter(val);
223
- }
224
- get highlight() {
225
- return this._manager?.propHighlightedId ?? null;
226
- }
227
- set highlight(val) {
228
- this.highlightAndScrollToElementOnTheList(val);
229
- }
230
- getManager() {
231
- return this._manager;
232
- }
233
- _parseJSON(val) {
234
- if (!val)
235
- return undefined;
236
- try {
237
- return JSON.parse(val);
238
- }
239
- catch (e) {
240
- console.error(`OptionsSection: failed to parse JSON:`, val, e);
241
- return undefined;
242
- }
243
- }
244
- }
245
- customElements.define("options-section", OptionsSection);
@@ -1,90 +0,0 @@
1
- // @ts-ignore
2
- import React from "react";
3
- import "./options-section.js";
4
- export const OptionsSection = React.forwardRef((props, ref) => {
5
- const { options: optionsOptions, loading, value, label, disabled, "max-height": maxHeight, "show-footer": setShowFooter, "show-filter": setShowFilter, children, onItemPick, onInputChange, onCancel, onOk, onHighlightChange, ...rest } = props;
6
- const internalRef = React.useRef(null);
7
- const setRef = React.useCallback((node) => {
8
- internalRef.current = node;
9
- if (typeof ref === "function") {
10
- ref(node);
11
- }
12
- else if (ref) {
13
- ref.current = node;
14
- }
15
- }, [ref]);
16
- React.useLayoutEffect(() => {
17
- const el = internalRef.current;
18
- if (el && optionsOptions !== undefined) {
19
- if (el.getManager && el.getManager()) {
20
- el.getManager().setOptions(typeof optionsOptions === "string" ? JSON.parse(optionsOptions) : optionsOptions);
21
- }
22
- else {
23
- el.setAttribute("options", typeof optionsOptions === "string" ? optionsOptions : JSON.stringify(optionsOptions));
24
- }
25
- }
26
- }, [optionsOptions]);
27
- React.useLayoutEffect(() => {
28
- const el = internalRef.current;
29
- if (el && el.getManager && el.getManager()) {
30
- const mgr = el.getManager();
31
- const sub = mgr.getSubscriber();
32
- if (sub) {
33
- const unbinds = [];
34
- if (onItemPick) {
35
- unbinds.push(sub.bind("onItemPick", (...args) => {
36
- return onItemPick?.(...args);
37
- }));
38
- }
39
- if (onInputChange) {
40
- unbinds.push(sub.bind("onInputChange", onInputChange));
41
- }
42
- if (onCancel) {
43
- unbinds.push(sub.bind("onCancel", onCancel));
44
- }
45
- if (onOk) {
46
- unbinds.push(sub.bind("onOk", onOk));
47
- }
48
- if (onHighlightChange) {
49
- unbinds.push(sub.bind("onHighlightChange", onHighlightChange));
50
- }
51
- return () => {
52
- unbinds.forEach((u) => u());
53
- };
54
- }
55
- }
56
- }, [onItemPick, onInputChange, onCancel, onOk, onHighlightChange]);
57
- const wcProps = { ...rest, ref: setRef };
58
- if (value !== undefined)
59
- wcProps.value = value;
60
- if (label !== undefined)
61
- wcProps.label = label;
62
- if (String(loading) === "true") {
63
- wcProps.loading = "true";
64
- }
65
- else {
66
- delete wcProps.loading;
67
- }
68
- if (String(disabled) === "true") {
69
- wcProps.disabled = "true";
70
- }
71
- else {
72
- delete wcProps.disabled;
73
- }
74
- if (maxHeight !== undefined)
75
- wcProps["max-height"] = maxHeight;
76
- if (String(setShowFooter) === "true") {
77
- wcProps["show-footer"] = "true";
78
- }
79
- else {
80
- delete wcProps["show-footer"];
81
- }
82
- if (String(setShowFilter) === "true") {
83
- wcProps["show-filter"] = "true";
84
- }
85
- else {
86
- delete wcProps["show-filter"];
87
- }
88
- return React.createElement("options-section", wcProps, children);
89
- });
90
- export default OptionsSection;
@@ -1,336 +0,0 @@
1
- import createSubscriber from "../createSubscriber.js";
2
- export class SelectedSectionManager {
3
- propParentElement;
4
- propContainer;
5
- propFlexList;
6
- propButtonsContainer;
7
- propOptions;
8
- propInputElement = null;
9
- propClearButton;
10
- propLoaderElement = null;
11
- propLabelElement = null;
12
- _skipNextFocusEvent = false;
13
- _subscriber = createSubscriber();
14
- constructor(parentElement, options = {}) {
15
- this.propParentElement = parentElement;
16
- const { selected = [], showInput = true, value = "", renderItem = (item, def) => def(item), renderList = (selected, def) => def(selected), onDelete = (id) => { }, onClear = () => { }, onInputChange = (e, previousValue) => { }, onChange = (selected) => { }, onComponentChange = (s, reason) => { }, disabled = false, error = false, loading = false, renderEmpty = (def) => def(), label = "", showDelete = true, ...rest } = options;
17
- this.propOptions = {
18
- selected,
19
- showInput,
20
- value,
21
- renderItem,
22
- renderList,
23
- onDelete,
24
- onClear,
25
- onInputChange,
26
- onChange,
27
- onComponentChange,
28
- disabled,
29
- error,
30
- loading,
31
- renderEmpty,
32
- label,
33
- showDelete,
34
- ...rest,
35
- };
36
- this.render();
37
- Promise.resolve().then(() => {
38
- this._bindEvents();
39
- });
40
- }
41
- getSubscriber() {
42
- return this._subscriber;
43
- }
44
- _bindEvents() {
45
- this.propParentElement.addEventListener("click", (e) => {
46
- const target = e.target;
47
- // Handle Clear Button
48
- const clearBtn = target.closest(".clear-btn");
49
- if (clearBtn && this.propParentElement.contains(clearBtn)) {
50
- e.preventDefault();
51
- this.clearSearch();
52
- return;
53
- }
54
- // Handle Delete Item
55
- const delBtn = target.closest("[data-remove]");
56
- if (delBtn && this.propParentElement.contains(delBtn)) {
57
- e.preventDefault();
58
- const id = delBtn.getAttribute("data-remove");
59
- if (this.propOptions.onDelete) {
60
- this.propOptions.onDelete.call(this, id);
61
- }
62
- this._subscriber.trigger("onDelete", id);
63
- return;
64
- }
65
- });
66
- this.propParentElement.addEventListener("input", (e) => {
67
- const target = e.target;
68
- if (target === this.propInputElement) {
69
- const previousValue = this.propOptions.value;
70
- this.propOptions.value = this.propInputElement.value;
71
- if (this.propOptions.onInputChange) {
72
- this.propOptions.onInputChange.call(this, e, previousValue);
73
- }
74
- this._subscriber.trigger("onInputChange", e, previousValue);
75
- }
76
- });
77
- this.propParentElement.addEventListener("focusin", (e) => {
78
- const target = e.target;
79
- if (target === this.propInputElement) {
80
- if (this._skipNextFocusEvent) {
81
- this._skipNextFocusEvent = false;
82
- return;
83
- }
84
- if (this.propOptions.onFocus) {
85
- this.propOptions.onFocus.call(this, e);
86
- }
87
- this._subscriber.trigger("onFocus", e);
88
- }
89
- });
90
- this.propParentElement.addEventListener("keydown", (e) => {
91
- const target = e.target;
92
- if (target === this.propInputElement) {
93
- const isBackspaceOnEmpty = e.key === "Backspace" && this.propInputElement.value === "";
94
- const isEnter = e.key === "Enter";
95
- if (isEnter || isBackspaceOnEmpty) {
96
- const previousValue = this.propOptions.value;
97
- if (this.propOptions.onInputChange) {
98
- this.propOptions.onInputChange.call(this, e, previousValue);
99
- }
100
- this._subscriber.trigger("onInputChange", e, previousValue);
101
- }
102
- }
103
- });
104
- }
105
- _triggerOnComponentChange(reason) {
106
- if (this.propOptions.onComponentChange) {
107
- this.propOptions.onComponentChange.call(this, this.propOptions, reason);
108
- }
109
- this._subscriber.trigger("onComponentChange", this.propOptions, reason);
110
- }
111
- _updateDeleteDisplay() {
112
- if (this.propContainer) {
113
- this.propContainer.classList.toggle("hide-delete", this.propOptions.showDelete === false);
114
- }
115
- }
116
- _defaultRenderList(selected) {
117
- return selected.map((item) => this.propOptions.renderItem(item, this._defaultRenderItem.bind(this)));
118
- }
119
- _defaultRenderItem(item) {
120
- const el = document.createElement("div");
121
- el.className = "element";
122
- el.dataset.id = String(item.id);
123
- const label = document.createElement("label");
124
- label.textContent = item.label;
125
- const del = document.createElement("div");
126
- del.dataset.remove = String(item.id);
127
- el.appendChild(label);
128
- el.appendChild(del);
129
- return el;
130
- }
131
- setSelected(list, triggerOnChange = true) {
132
- this.propOptions.selected = list.map((s) => {
133
- s.selected = true;
134
- return s;
135
- });
136
- this.render();
137
- if (triggerOnChange) {
138
- if (this.propOptions.onChange) {
139
- this.propOptions.onChange.call(this, this.getSelected());
140
- }
141
- this._subscriber.trigger("onChange", this.getSelected());
142
- }
143
- this._triggerOnComponentChange("setSelected");
144
- }
145
- setShowInput(show) {
146
- this.propOptions.showInput = show = String(show) === "true" || show === true;
147
- this.propInputElement.style.display = show ? "" : "none";
148
- this.propInputElement.disabled = Boolean(this.propOptions.disabled);
149
- this._triggerOnComponentChange("setShowInput");
150
- }
151
- setValue(value, triggerOnChange = true) {
152
- const previousValue = this.propOptions.value;
153
- this.propOptions.value = value;
154
- this.propInputElement.value = value;
155
- if (triggerOnChange) {
156
- const e = new Event("input");
157
- Object.defineProperty(e, "target", { writable: false, value: this.propInputElement });
158
- if (this.propOptions.onInputChange) {
159
- this.propOptions.onInputChange.call(this, e, previousValue);
160
- }
161
- this._subscriber.trigger("onInputChange", e, previousValue);
162
- }
163
- this._triggerOnComponentChange("setValue");
164
- }
165
- setFocus(triggerOnFocus = true) {
166
- if (!triggerOnFocus) {
167
- this._skipNextFocusEvent = true;
168
- }
169
- this.propInputElement.focus();
170
- }
171
- clearSearch(triggerOnClear = true, triggerOnChange = true) {
172
- this.setValue("", triggerOnChange);
173
- if (triggerOnClear) {
174
- if (this.propOptions.onClear) {
175
- this.propOptions.onClear.call(this);
176
- }
177
- this._subscriber.trigger("onClear");
178
- }
179
- this._triggerOnComponentChange("onClear");
180
- }
181
- setLabel(label) {
182
- this.propOptions.label = label;
183
- if (label) {
184
- if (!this.propLabelElement) {
185
- this.propLabelElement = document.createElement("label");
186
- this.propLabelElement.className = "floating-label";
187
- }
188
- if (this.propLabelElement.parentNode !== this.propContainer) {
189
- this.propContainer.insertBefore(this.propLabelElement, this.propContainer.firstChild);
190
- }
191
- this.propLabelElement.textContent = label;
192
- }
193
- else {
194
- if (this.propLabelElement && this.propLabelElement.parentNode === this.propContainer) {
195
- this.propContainer.removeChild(this.propLabelElement);
196
- }
197
- }
198
- this._triggerOnComponentChange("setLabel");
199
- }
200
- setError(isError) {
201
- this.propOptions.error = Boolean(isError);
202
- this.propContainer.classList.toggle("error", isError);
203
- this._triggerOnComponentChange("setError");
204
- }
205
- setDisabled(disabled) {
206
- this.propOptions.disabled = Boolean(disabled);
207
- this.propInputElement.disabled = disabled;
208
- this.propClearButton.disabled = disabled;
209
- this.propContainer.classList.toggle("disabled", disabled);
210
- this._triggerOnComponentChange("setDisabled");
211
- }
212
- /**
213
- * show/hide delete icon
214
- * @param show
215
- */
216
- setShowDelete(show) {
217
- this.propOptions.showDelete = Boolean(show);
218
- this._updateDeleteDisplay();
219
- this._triggerOnComponentChange("setShowDelete");
220
- }
221
- setLoading(loading) {
222
- this.propOptions.loading = Boolean(loading);
223
- if (loading) {
224
- if (!this.propLoaderElement) {
225
- this.propLoaderElement = document.createElement("div");
226
- this.propLoaderElement.className = "loader";
227
- }
228
- if (this.propClearButton.parentNode) {
229
- this.propButtonsContainer.replaceChild(this.propLoaderElement, this.propClearButton);
230
- }
231
- }
232
- else {
233
- if (this.propLoaderElement && this.propLoaderElement.parentNode) {
234
- this.propButtonsContainer.replaceChild(this.propClearButton, this.propLoaderElement);
235
- }
236
- }
237
- this._triggerOnComponentChange("setLoading");
238
- }
239
- render() {
240
- if (!this.propContainer) {
241
- this.propContainer = document.createElement("div");
242
- this.propContainer.className = "selected-section";
243
- this.propFlexList = document.createElement("div");
244
- this.propFlexList.className = "flex-list";
245
- this.propButtonsContainer = document.createElement("div");
246
- this.propButtonsContainer.className = "buttons-container";
247
- this.propClearButton = document.createElement("button");
248
- this.propClearButton.className = "clear-btn";
249
- this.propClearButton.textContent = "✕";
250
- this.propButtonsContainer.appendChild(this.propClearButton);
251
- this.propContainer.appendChild(this.propFlexList);
252
- this.propContainer.appendChild(this.propButtonsContainer);
253
- this.propParentElement.appendChild(this.propContainer);
254
- const input = document.createElement("input");
255
- input.type = "text";
256
- input.value = this.propOptions.value || "";
257
- input.placeholder = " ";
258
- input.size = 1;
259
- this.propInputElement = input;
260
- this.propFlexList.appendChild(this.propInputElement);
261
- }
262
- this.setLabel(this.propOptions.label || "");
263
- this.setShowDelete(this.propOptions.showDelete !== false);
264
- this.setError(Boolean(this.propOptions.error));
265
- this.setDisabled(Boolean(this.propOptions.disabled));
266
- this.setLoading(Boolean(this.propOptions.loading));
267
- this.setShowInput(Boolean(this.propOptions.showInput));
268
- const elements = this.propOptions.renderList(this.getSelected(), this._defaultRenderList.bind(this));
269
- if (!Array.isArray(elements)) {
270
- throw new Error("renderList must return an array of HTMLElements");
271
- }
272
- // clear the list (except touching input)
273
- const children = Array.from(this.propFlexList.childNodes);
274
- for (const child of children) {
275
- if (child instanceof HTMLElement && child.tagName === "INPUT") {
276
- continue;
277
- }
278
- this.propFlexList.removeChild(child);
279
- }
280
- // rendering elements in order before input
281
- for (const el of elements) {
282
- if (!(el instanceof HTMLElement)) {
283
- throw new Error("each element returned from renderList must be an HTMLElement");
284
- }
285
- if (this.propInputElement && this.propInputElement.parentNode === this.propFlexList) {
286
- this.propFlexList.insertBefore(el, this.propInputElement);
287
- }
288
- else {
289
- this.propFlexList.appendChild(el);
290
- }
291
- }
292
- }
293
- setRenderItem(renderItem) {
294
- this.propOptions.renderItem = renderItem || ((item, def) => def(item));
295
- this.render();
296
- this._triggerOnComponentChange("setRenderItem");
297
- }
298
- setRenderList(renderList) {
299
- this.propOptions.renderList = renderList || ((selected, def) => def(selected));
300
- this.render();
301
- this._triggerOnComponentChange("setRenderList");
302
- }
303
- setRenderEmpty(renderEmpty) {
304
- this.propOptions.renderEmpty = renderEmpty || ((def) => def());
305
- this.render();
306
- this._triggerOnComponentChange("setRenderEmpty");
307
- }
308
- // getters
309
- getSelected() {
310
- return this.propOptions.selected || [];
311
- }
312
- getShowInput() {
313
- return this.propOptions.showInput;
314
- }
315
- getValue() {
316
- return this.propOptions.value;
317
- }
318
- getLabel() {
319
- return this.propOptions.label;
320
- }
321
- getDisabled() {
322
- return this.propOptions.disabled;
323
- }
324
- getError() {
325
- return this.propOptions.error;
326
- }
327
- getLoading() {
328
- return this.propOptions.loading;
329
- }
330
- getShowDelete() {
331
- return this.propOptions.showDelete;
332
- }
333
- destroy() {
334
- this._subscriber.destroy();
335
- }
336
- }