@andreyshpigunov/x 0.3.72

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.
Files changed (60) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/publish.yml +28 -0
  3. package/README.md +37 -0
  4. package/assets/alpha.png +0 -0
  5. package/assets/apple-touch-icon.png +0 -0
  6. package/assets/github-mark-white.png +0 -0
  7. package/assets/github-mark.png +0 -0
  8. package/assets/logo-inverse.png +0 -0
  9. package/assets/logo.png +0 -0
  10. package/assets/logo.svg +9 -0
  11. package/babel.config.cjs +4 -0
  12. package/dist/app.css +167 -0
  13. package/dist/app.js +1 -0
  14. package/dist/x.css +167 -0
  15. package/dist/x.js +1 -0
  16. package/favicon.ico +0 -0
  17. package/favicon.svg +9 -0
  18. package/index.html +2214 -0
  19. package/index.js +1 -0
  20. package/jest.config.mjs +7 -0
  21. package/jsdoc.json +11 -0
  22. package/package.json +50 -0
  23. package/src/components/x/animate.js +296 -0
  24. package/src/components/x/appear.js +158 -0
  25. package/src/components/x/autocomplete.js +150 -0
  26. package/src/components/x/buttons.css +265 -0
  27. package/src/components/x/colors.css +64 -0
  28. package/src/components/x/debug.css +55 -0
  29. package/src/components/x/device.js +265 -0
  30. package/src/components/x/dropdown.css +164 -0
  31. package/src/components/x/dropdown.js +463 -0
  32. package/src/components/x/flex.css +163 -0
  33. package/src/components/x/flow.css +52 -0
  34. package/src/components/x/form.css +138 -0
  35. package/src/components/x/form.js +180 -0
  36. package/src/components/x/grid.css +109 -0
  37. package/src/components/x/helpers.css +928 -0
  38. package/src/components/x/hover.js +93 -0
  39. package/src/components/x/icons.css +58 -0
  40. package/src/components/x/lazyload.js +153 -0
  41. package/src/components/x/lib.js +679 -0
  42. package/src/components/x/links.css +114 -0
  43. package/src/components/x/loadmore.js +191 -0
  44. package/src/components/x/modal.css +286 -0
  45. package/src/components/x/modal.js +346 -0
  46. package/src/components/x/reset.css +213 -0
  47. package/src/components/x/scroll.css +100 -0
  48. package/src/components/x/scroll.js +301 -0
  49. package/src/components/x/sheets.css +15 -0
  50. package/src/components/x/sheets.js +147 -0
  51. package/src/components/x/slider.css +83 -0
  52. package/src/components/x/slider.js +330 -0
  53. package/src/components/x/space.css +56 -0
  54. package/src/components/x/sticky.css +28 -0
  55. package/src/components/x/sticky.js +156 -0
  56. package/src/components/x/typo.css +318 -0
  57. package/src/css/app.css +407 -0
  58. package/src/css/x.css +252 -0
  59. package/src/js/app.js +47 -0
  60. package/src/js/x.js +81 -0
package/index.js ADDED
@@ -0,0 +1 @@
1
+ import './dist/x.js';
@@ -0,0 +1,7 @@
1
+ // jest.config.mjs
2
+ export default {
3
+ transform: {
4
+ '^.+\\.js$': 'babel-jest',
5
+ },
6
+ testEnvironment: 'jsdom'
7
+ };
package/jsdoc.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "source": {
3
+ "include": ["src/components/x"],
4
+ "includePattern": ".js$"
5
+ },
6
+ "opts": {
7
+ "destination": "./docs",
8
+ "recurse": true,
9
+ "template": "node_modules/docdash"
10
+ }
11
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@andreyshpigunov/x",
3
+ "version": "0.3.72",
4
+ "devDependencies": {
5
+ "@babel/preset-env": "^7.26.9",
6
+ "@jest/globals": "^29.7.0",
7
+ "@parcel/logger": "*",
8
+ "autoprefixer": "^10.1.0",
9
+ "babel-jest": "^29.7.0",
10
+ "cssnano": "^5.0.2",
11
+ "docdash": "^2.0.2",
12
+ "jest": "^29.7.0",
13
+ "jest-environment-jsdom": "^29.7.0",
14
+ "jsdoc": "^4.0.4",
15
+ "parcel": "^2.0.1",
16
+ "postcss": "^8.5",
17
+ "postcss-cli": "^8.3.0",
18
+ "postcss-custom-media": "^11.0.1",
19
+ "postcss-custom-selectors": "^8.0.1",
20
+ "postcss-each": "^1.1.0",
21
+ "postcss-extend": "^1.0.5",
22
+ "postcss-for": "^2.1.1",
23
+ "postcss-import": "^14.0.2",
24
+ "postcss-nesting": "^10.2.0",
25
+ "postcss-simple-vars": "^7.0.1"
26
+ },
27
+ "type": "module",
28
+ "scripts": {
29
+ "build-css": "postcss src/css/*.css -u postcss-import -u postcss-nesting -u postcss-custom-media -u postcss-custom-selectors -u postcss-extend -u postcss-for -u postcss-each -u autoprefixer -u cssnano --no-map -d dist/",
30
+ "build-js": "parcel build src/js/*.js --no-source-maps --no-cache --dist-dir dist/",
31
+ "build": "npm run build-css && npm run build-js",
32
+ "test": "exit 0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/andreyshpigunov/x.git"
37
+ },
38
+ "author": "Andrey Shpigunov",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/andreyshpigunov/x/issues"
42
+ },
43
+ "homepage": "https://andreyshpigunov.github.io/x/",
44
+ "browserslist": [
45
+ "defaults"
46
+ ],
47
+ "dependencies": {
48
+ "alpinejs": "^3.14.1"
49
+ }
50
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * @fileoverview Scroll-based animation controller.
3
+ *
4
+ * Observes elements with `[x-animate]` attribute and applies classes or executes functions
5
+ * based on the element's position in the viewport or parent container.
6
+ *
7
+ * Exported singleton: `animate`
8
+ *
9
+ * Public API:
10
+ *
11
+ * - `animate.init()` – Initialize/reinitialize animation tracking for `[x-animate]` elements.
12
+ *
13
+ * Example usage:
14
+ *
15
+ * <div x-animate='{
16
+ * "parent": "#scroll-container",
17
+ * "trigger": ".trigger",
18
+ * "start": "120vh",
19
+ * "end": "0vh",
20
+ * "functionName": "coverOut",
21
+ * "class": "fixed",
22
+ * "classRemove": true
23
+ * }'></div>
24
+ *
25
+ * @author Andrey Shpigunov
26
+ * @version 0.3
27
+ * @since 2025-07-18
28
+ */
29
+
30
+ import { lib } from './lib';
31
+
32
+ /**
33
+ * Scroll-based animation controller.
34
+ */
35
+ class Animate {
36
+
37
+ constructor() {
38
+ /**
39
+ * Prevents multiple `requestAnimationFrame` calls.
40
+ * @type {boolean}
41
+ * @private
42
+ */
43
+ this._ticking = false;
44
+
45
+ /**
46
+ * Array of animation items parsed from `[x-animate]` elements.
47
+ * @type {Object[]}
48
+ * @private
49
+ */
50
+ this._animations = [];
51
+
52
+ /**
53
+ * Bound scroll handler for `requestAnimationFrame`.
54
+ * @type {Function}
55
+ * @private
56
+ */
57
+ this._scroll = this._scroll.bind(this);
58
+
59
+ /**
60
+ * Bound raw scroll/resize handler.
61
+ * @type {Function}
62
+ * @private
63
+ */
64
+ this._scrollHandler = this._scrollHandler.bind(this);
65
+
66
+ /**
67
+ * Indicates whether `init()` was called.
68
+ * @type {boolean}
69
+ * @private
70
+ */
71
+ this._initialized = false;
72
+
73
+ /**
74
+ * Set of parent elements being listened to for scroll.
75
+ * @type {Set<HTMLElement|Window>}
76
+ * @private
77
+ */
78
+ this._parents = new Set();
79
+
80
+ /**
81
+ * NodeList of elements with `[x-animate]`.
82
+ * @type {NodeListOf<HTMLElement>|null}
83
+ * @private
84
+ */
85
+ this._elements = null;
86
+ }
87
+
88
+ /**
89
+ * Initializes or reinitializes animation tracking for `[x-animate]` elements.
90
+ */
91
+ init() {
92
+ this._cleanup();
93
+
94
+ this._elements = lib.qsa('[x-animate]');
95
+ if (!this._elements?.length) return;
96
+
97
+ this._parseElementsAnimations();
98
+ if (!this._animations.length) return;
99
+
100
+ this._setupListeners();
101
+ this._initialized = true;
102
+ }
103
+
104
+ /**
105
+ * Removes all listeners and resets internal state.
106
+ *
107
+ * @private
108
+ */
109
+ _cleanup() {
110
+ if (!this._initialized) return;
111
+
112
+ this._parents.forEach(parent => {
113
+ parent.removeEventListener('scroll', this._scrollHandler);
114
+ });
115
+
116
+ window.removeEventListener('resize', this._scrollHandler);
117
+
118
+ this._ticking = false;
119
+ this._animations = [];
120
+ this._parents = new Set();
121
+ this._elements = null;
122
+ this._initialized = false;
123
+ }
124
+
125
+ /**
126
+ * Parses `[x-animate]` attributes and creates animation configuration for each element.
127
+ *
128
+ * @private
129
+ */
130
+ _parseElementsAnimations() {
131
+ this._elements.forEach(element => {
132
+ try {
133
+ const json = JSON.parse(element.getAttribute('x-animate'));
134
+ const item = {
135
+ element,
136
+ trigger: lib.qs(json.trigger) || element,
137
+ parent: lib.qs(json.parent) || window,
138
+ start: json.start,
139
+ end: json.end || false,
140
+ class: json.class,
141
+ classRemove: json.classRemove !== false,
142
+ functionName: json.functionName,
143
+ lockedIn: false,
144
+ lockedOut: false,
145
+ log: json.log || false
146
+ };
147
+ this._animations.push(item);
148
+ } catch (err) {
149
+ console.error('Invalid JSON in x-animate attribute:', element, err);
150
+ }
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Sets up scroll and resize event listeners for unique parent containers.
156
+ *
157
+ * @private
158
+ */
159
+ _setupListeners() {
160
+ for (const item of this._animations) {
161
+ if (this._parents.has(item.parent)) continue;
162
+ this._parents.add(item.parent);
163
+ item.parent.addEventListener('scroll', this._scrollHandler);
164
+ }
165
+
166
+ window.addEventListener('resize', this._scrollHandler);
167
+
168
+ if (document.readyState === 'complete') {
169
+ requestAnimationFrame(() => this._scroll());
170
+ } else {
171
+ window.addEventListener('load', () => this._scroll(), { once: true });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Raw scroll/resize event handler with throttling via `requestAnimationFrame`.
177
+ *
178
+ * @private
179
+ */
180
+ _scrollHandler() {
181
+ if (!this._ticking) {
182
+ this._ticking = true;
183
+ window.requestAnimationFrame(() => {
184
+ this._scroll();
185
+ this._ticking = false;
186
+ });
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Main animation logic executed on scroll or resize.
192
+ *
193
+ * Calculates element position, progress, adds/removes classes, and calls custom functions.
194
+ *
195
+ * @private
196
+ */
197
+ _scroll() {
198
+ this._animations.forEach(item => {
199
+ const triggerRect = item.trigger.getBoundingClientRect();
200
+ const parentRect = item.parent !== window ? item.parent.getBoundingClientRect() : null;
201
+
202
+ const top = triggerRect.top - (parentRect ? parentRect.top : 0);
203
+ const start = this._2px(item.start, item.parent);
204
+ const end = this._2px(item.end, item.parent);
205
+ item.duration = isNaN(end) ? 0 : start - end;
206
+
207
+ if (item.log) console.log(top, start, end, item);
208
+
209
+ if (!isNaN(start) && !isNaN(end)) {
210
+ // Case: both start and end defined
211
+ if (top <= start && top >= end) {
212
+ item.lockedOut = false;
213
+ if (item.class) item.element.classList.add(item.class);
214
+
215
+ if (typeof window[item.functionName] === 'function') {
216
+ item.progress = ((start - top) / item.duration).toFixed(4);
217
+ window[item.functionName](item);
218
+ }
219
+
220
+ } else {
221
+ if (item.class && item.classRemove === true && item.element.classList.contains(item.class)) {
222
+ item.element.classList.remove(item.class);
223
+ }
224
+
225
+ if (!item.lockedOut && typeof window[item.functionName] === 'function') {
226
+ if (top >= start) {
227
+ item.progress = 0;
228
+ window[item.functionName](item);
229
+ item.lockedOut = true;
230
+ }
231
+ if (top <= end) {
232
+ item.progress = 1;
233
+ window[item.functionName](item);
234
+ item.lockedOut = true;
235
+ }
236
+ }
237
+ }
238
+
239
+ } else if (!isNaN(start)) {
240
+ // Case: only start defined
241
+ if (top <= start) {
242
+ item.lockedOut = false;
243
+ if (item.class) item.element.classList.add(item.class);
244
+
245
+ if (!item.lockedIn && typeof window[item.functionName] === 'function') {
246
+ item.progress = 1;
247
+ window[item.functionName](item);
248
+ item.lockedIn = true;
249
+ }
250
+
251
+ } else {
252
+ item.lockedIn = false;
253
+
254
+ if (item.class && item.classRemove === true && item.element.classList.contains(item.class)) {
255
+ item.element.classList.remove(item.class);
256
+ }
257
+
258
+ if (!item.lockedOut && typeof window[item.functionName] === 'function') {
259
+ if (top >= start) {
260
+ item.progress = 0;
261
+ window[item.functionName](item);
262
+ item.lockedOut = true;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Converts a value like '120vh', '50%' or '300' into pixels.
272
+ *
273
+ * @param {string|number} value - The value to convert.
274
+ * @param {HTMLElement|Window} [parent=window] - The context for percentage calculations.
275
+ * @returns {number} Pixel value.
276
+ * @private
277
+ */
278
+ _2px(value, parent = window) {
279
+ if (/(%|vh)/.test(value)) {
280
+ const height = parent === window
281
+ ? document.documentElement.clientHeight
282
+ : parent.clientHeight;
283
+
284
+ value = value.replace(/(vh|%)/, '');
285
+ return (height * parseFloat(value)) / 100;
286
+ } else {
287
+ return parseFloat(value);
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Singleton export of the Animate controller.
294
+ * @type {Animate}
295
+ */
296
+ export const animate = new Animate();
@@ -0,0 +1,158 @@
1
+ /**
2
+ * @fileoverview Element appearing and visibility observer.
3
+ *
4
+ * Detects when elements enter or leave the viewport and applies CSS classes accordingly.
5
+ * Intended for triggering animations, lazy loading, or visual effects.
6
+ *
7
+ * Exported singleton: `appear`
8
+ *
9
+ * Public API:
10
+ *
11
+ * - `appear.init(config)` – Initializes or reinitializes the observer.
12
+ *
13
+ * Usage example:
14
+ *
15
+ * HTML:
16
+ * <div x-appear>Hello!</div>
17
+ *
18
+ * Behavior:
19
+ * - Adds `appeared` class once when the element first appears.
20
+ * - Adds `visible` class while the element is currently visible.
21
+ *
22
+ * Configuration via `init()`:
23
+ * - `appearedClass` – Custom class for the first appearance (default: 'appeared').
24
+ * - `visibleClass` – Custom class for current visibility (default: 'visible').
25
+ * - `once` – If `true`, stops observing the element after first appearance.
26
+ *
27
+ * Events:
28
+ * - `visible` – Dispatched when the element becomes visible.
29
+ * - `invisible` – Dispatched when the element leaves the viewport.
30
+ *
31
+ * @author Andrey Shpigunov
32
+ * @version 0.3
33
+ * @since 2025-07-18
34
+ */
35
+
36
+ import { lib } from './lib';
37
+
38
+ /**
39
+ * Element appearing and visibility observer.
40
+ *
41
+ * Uses IntersectionObserver to track elements with [x-appear] attribute and manage classes.
42
+ */
43
+ class Appear {
44
+
45
+ constructor() {
46
+ /**
47
+ * List of currently observed elements.
48
+ * @type {HTMLElement[]}
49
+ * @private
50
+ */
51
+ this._targets = [];
52
+
53
+ /**
54
+ * Instance of IntersectionObserver.
55
+ * @type {IntersectionObserver|null}
56
+ * @private
57
+ */
58
+ this._observer = null;
59
+
60
+ /**
61
+ * Observer options and behavior configuration.
62
+ * @type {{
63
+ * appearedClass: string,
64
+ * visibleClass: string,
65
+ * once: boolean
66
+ * }}
67
+ * @private
68
+ */
69
+ this._options = {
70
+ appearedClass: 'appeared',
71
+ visibleClass: 'visible',
72
+ once: false
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Initializes or reinitializes the observer and starts observing elements.
78
+ *
79
+ * @param {Object} [config={}] - Optional configuration object.
80
+ * @param {string} [config.appearedClass='appeared'] - Class added once when the element first appears.
81
+ * @param {string} [config.visibleClass='visible'] - Class added while the element is visible.
82
+ * @param {boolean} [config.once=false] - If true, stops observing after first appearance.
83
+ *
84
+ * @example
85
+ * appear.init({ once: true });
86
+ */
87
+ init(config = {}) {
88
+ if (!('IntersectionObserver' in window)) return;
89
+
90
+ this._options = { ...this._options, ...config };
91
+
92
+ // Disconnect previous observer to prevent leaks
93
+ if (this._observer) {
94
+ this._observer.disconnect();
95
+ this._observer = null;
96
+ }
97
+
98
+ this._targets = lib.qsa('[x-appear]');
99
+
100
+ if (this._targets.length) {
101
+ this._observer = new IntersectionObserver(this._observerCallback.bind(this));
102
+
103
+ this._targets.forEach(item => {
104
+ this._observer.observe(item);
105
+ });
106
+ }
107
+ }
108
+
109
+ /**
110
+ * IntersectionObserver callback. Handles visibility changes for tracked elements.
111
+ *
112
+ * @param {IntersectionObserverEntry[]} entries - Array of observer entries.
113
+ * @private
114
+ */
115
+ _observerCallback(entries) {
116
+ const { appearedClass, visibleClass, once } = this._options;
117
+
118
+ for (const entry of entries) {
119
+ const target = entry.target;
120
+
121
+ const hasAppeared = appearedClass != null;
122
+ const hasVisible = visibleClass != null;
123
+
124
+ if (entry.isIntersecting) {
125
+ // First time visibility — add appeared class
126
+ if (hasAppeared && !target.classList.contains(appearedClass)) {
127
+ target.classList.add(appearedClass);
128
+
129
+ if (once) {
130
+ this._observer.unobserve(target);
131
+ }
132
+ }
133
+
134
+ // While visible — add visible class and dispatch event
135
+ if (hasVisible) {
136
+ target.classList.add(visibleClass);
137
+ target.dispatchEvent(new CustomEvent('visible', {
138
+ detail: { appeared: true }
139
+ }));
140
+ }
141
+ } else {
142
+ // When leaving viewport — remove visible class and dispatch event
143
+ if (hasVisible && target.classList.contains(visibleClass)) {
144
+ target.classList.remove(visibleClass);
145
+ target.dispatchEvent(new CustomEvent('invisible', {
146
+ detail: { appeared: true }
147
+ }));
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Singleton export of the Appear observer.
156
+ * @type {Appear}
157
+ */
158
+ export const appear = new Appear();
@@ -0,0 +1,150 @@
1
+ import { lib } from './lib';
2
+
3
+ class Autocomplete {
4
+
5
+ emptyStateHtml = `<li><span class="op4">Ничего не найдено</span></li>`;
6
+ defaultStateHtml = `<li><span class="op4">Начните печатать</span></li>`;
7
+ loadingStateHtml = `<li><span class="op4">Загрузка...</span></li>`;
8
+
9
+ init(dropdownId, options) {
10
+ this.emptyStateHtml = options.emptyStateHtml || this.emptyStateHtml;
11
+ this.defaultStateHtml = options.defaultStateHtml || this.defaultStateHtml;
12
+ this.loadingStateHtml = options.loadingStateHtml || this.loadingStateHtml;
13
+
14
+ this.dropdown = x.id(dropdownId);
15
+ this.field = x.qs('[x-dropdown-open]', this.dropdown);
16
+ this.list = x.qs('[x-dropdown]', this.dropdown);
17
+
18
+ this.data = options.data || null;
19
+ this.loadData = options.loadData || null;
20
+ this.mapData = options.mapData || null;
21
+ this.renderItem = options.renderItem || null;
22
+ this.onSelect = options.onSelect || null;
23
+ this.resetFunc = options.resetFunc || null;
24
+
25
+ this._loadData = this._loadData.bind(this);
26
+ this.debouncedLoadData = x.lib.debounce(this._loadData, 400);
27
+
28
+ this.keyHandler = this._keyHandler.bind(this);
29
+ this.clickHandler = this._clickHandler.bind(this);
30
+ this.hideHandler = this._hideHandler.bind(this);
31
+
32
+ if (this.field?.value == '') {
33
+ this._reset();
34
+ } else {
35
+ this._defaultState();
36
+ }
37
+ this.field.addEventListener('focus', this.debouncedLoadData);
38
+ this.field.addEventListener('input', this.debouncedLoadData);
39
+ this.field.addEventListener('keydown', this.keyHandler);
40
+ this.list.addEventListener('click', this.clickHandler);
41
+ this.dropdown.addEventListener('dropdown:afterhide', this.hideHandler);
42
+ }
43
+
44
+ destroy() {
45
+ this._reset();
46
+ this.field.removeEventListener('focus', this.debouncedLoadData);
47
+ this.field.removeEventListener('input', this.debouncedLoadData);
48
+ this.field.removeEventListener('keydown', this.keyHandler);
49
+ this.list.removeEventListener('click', this.clickHandler);
50
+ this.dropdown.removeEventListener('dropdown:afterhide', this.hideHandler);
51
+ }
52
+
53
+ _keyHandler(e) {
54
+ if (e.key === 'Enter') {
55
+ e.preventDefault();
56
+ this.field.blur();
57
+ }
58
+ }
59
+
60
+ _clickHandler(e) {
61
+ if (this._clicked) return;
62
+ this._clicked = true;
63
+
64
+ const el = e.target.closest('[data-item]');
65
+ if (el) {
66
+ try {
67
+ const item = JSON.parse(el.dataset.item);
68
+ this.onSelect(item);
69
+ } catch(err) {
70
+ console.error('Error parsing JSON');
71
+ }
72
+ } else {
73
+ console.log('Empty data-item');
74
+ }
75
+ }
76
+
77
+ _hideHandler() {
78
+ if (this._clicked) {
79
+ this._clicked = false;
80
+ return;
81
+ }
82
+
83
+ if (!this.data || !this.data.length) {
84
+ this._reset();
85
+ return;
86
+ }
87
+ this._loadingState();
88
+ this.onSelect(this.data[0]);
89
+ }
90
+
91
+ _loadingState() {
92
+ x.lib.render(this.list, this.loadingStateHtml);
93
+ }
94
+
95
+ _emptyState() {
96
+ x.lib.render(this.list, this.emptyStateHtml);
97
+ }
98
+
99
+ _defaultState() {
100
+ x.lib.render(this.list, this.defaultStateHtml);
101
+ }
102
+
103
+ _reset() {
104
+ this.resetFunc();
105
+ this.data = null;
106
+ this._defaultState();
107
+ }
108
+
109
+ async _loadData() {
110
+ if (this.field?.value == '') {
111
+ this._reset();
112
+ return;
113
+ } else {
114
+ this._loadingState();
115
+ }
116
+
117
+ try {
118
+ this._currentLoadId = Date.now();
119
+ const loadId = this._currentLoadId;
120
+
121
+ this.data = await this.loadData(this);
122
+
123
+ if (loadId !== this._currentLoadId) return;
124
+
125
+ this.data = this.mapData(this.data);
126
+ this.render();
127
+ } catch(err) {
128
+ this._reset();
129
+ console.error('Data loading error:', err);
130
+ }
131
+ }
132
+
133
+ async render() {
134
+ if (!this.data) {
135
+ this._reset();
136
+ return;
137
+ }
138
+
139
+ if (!this.data.length) {
140
+ this._emptyState();
141
+ return;
142
+ }
143
+
144
+ const html = this.data.map(this.renderItem).join('');
145
+
146
+ x.lib.render(this.list, html);
147
+ }
148
+ }
149
+
150
+ export const autocomplete = new Autocomplete();