vgapp 1.2.5 → 1.2.6

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 (42) hide show
  1. package/CHANGELOG.md +58 -35
  2. package/agents.md +2 -2
  3. package/app/modules/base-module.js +87 -10
  4. package/app/modules/vgalert/js/vgalert.js +3 -3
  5. package/app/modules/vgdynamictable/index.js +5 -0
  6. package/app/modules/vgdynamictable/js/editable.js +438 -0
  7. package/app/modules/vgdynamictable/js/expandable.js +248 -0
  8. package/app/modules/vgdynamictable/js/filters.js +450 -0
  9. package/app/modules/vgdynamictable/js/fixed.js +566 -0
  10. package/app/modules/vgdynamictable/js/options.js +646 -0
  11. package/app/modules/vgdynamictable/js/pagination.js +623 -0
  12. package/app/modules/vgdynamictable/js/search.js +82 -0
  13. package/app/modules/vgdynamictable/js/skeleton.js +136 -0
  14. package/app/modules/vgdynamictable/js/sortable.js +442 -0
  15. package/app/modules/vgdynamictable/js/summary-footer.js +284 -0
  16. package/app/modules/vgdynamictable/js/table-remote.js +821 -0
  17. package/app/modules/vgdynamictable/js/table-state.js +243 -0
  18. package/app/modules/vgdynamictable/js/table-url-state.js +444 -0
  19. package/app/modules/vgdynamictable/js/utils/common.js +48 -0
  20. package/app/modules/vgdynamictable/js/vgdynamictable.js +3829 -0
  21. package/app/modules/vgdynamictable/js/viewport.js +322 -0
  22. package/app/modules/vgdynamictable/readme.md +251 -0
  23. package/app/modules/vgdynamictable/scss/_actions.scss +193 -0
  24. package/app/modules/vgdynamictable/scss/_pagination.scss +183 -0
  25. package/app/modules/vgdynamictable/scss/_skeleton.scss +60 -0
  26. package/app/modules/vgdynamictable/scss/_sortable.scss +63 -0
  27. package/app/modules/vgdynamictable/scss/_table.scss +157 -0
  28. package/app/modules/vgdynamictable/scss/_variables.scss +130 -0
  29. package/app/modules/vgdynamictable/scss/vgdynamictable.scss +267 -0
  30. package/app/modules/vgrangeslider/index.js +3 -0
  31. package/app/modules/vgrangeslider/js/skins.js +222 -0
  32. package/app/modules/vgrangeslider/js/vgrangeslider.js +704 -0
  33. package/app/modules/vgrangeslider/readme.md +523 -0
  34. package/app/modules/vgrangeslider/scss/_variables.scss +53 -0
  35. package/app/modules/vgrangeslider/scss/vgrangeslider.scss +240 -0
  36. package/app/utils/js/components/ajax.js +116 -7
  37. package/build/vgapp.css +2 -3246
  38. package/build/vgapp.css.map +1 -1
  39. package/build/vgapp.js +2 -30
  40. package/index.js +2 -0
  41. package/index.scss +6 -0
  42. package/package.json +1 -1
@@ -0,0 +1,623 @@
1
+ import VGDropdown from "../../vgdropdown";
2
+
3
+ class Pagination {
4
+ constructor(el, params = {}) {
5
+ if (!el || !(el instanceof Element)) {
6
+ throw new Error('Pagination requires a valid root element');
7
+ }
8
+
9
+ const defaults = {
10
+ markupSelector: '[data-vgdt-pagination], .vgdt-pagination',
11
+ perPageOptionSuffix: 'page',
12
+ showPerPageLabel: false,
13
+ perPageLabel: 'Строк на странице',
14
+ prevLabel: 'Назад',
15
+ nextLabel: 'Вперед',
16
+ quickJumpButtonLabel: 'Перейти',
17
+ container: null,
18
+ maxPerPage: 100,
19
+ };
20
+
21
+ this._root = el;
22
+ this._params = Object.assign({}, defaults, params);
23
+
24
+ this._maxPerPage = this._resolveMaxPerPage();
25
+ this._state = {
26
+ page: this._normalizeInt(this._params.page, 1),
27
+ perPage: this._normalizePerPage(this._params.perPage, 10),
28
+ totalPages: this._normalizeInt(this._params.totalPages, 1),
29
+ };
30
+
31
+ this._containers = [];
32
+ this._lastMarkupByContainer = new WeakMap();
33
+ this._boundContainerClick = this._handleContainerClick.bind(this);
34
+ this._boundContainerChange = this._handleContainerChange.bind(this);
35
+ this._boundContainerKeyDown = this._handleContainerKeyDown.bind(this);
36
+ this._boundContainerFocusIn = this._handleContainerFocusIn.bind(this);
37
+ this._boundContainerBlur = this._handleContainerBlur.bind(this);
38
+ this._dropdownInstances = new WeakMap();
39
+ }
40
+
41
+ init() {
42
+ this._containers = this._resolveContainers();
43
+ if (!this._containers.length) return;
44
+ this._render();
45
+ this._bindEvents();
46
+ }
47
+
48
+ setMeta(meta = {}) {
49
+ let changed = false;
50
+ if (meta.page !== undefined) {
51
+ const nextPage = this._normalizeInt(meta.page, this._state.page);
52
+ if (nextPage !== this._state.page) {
53
+ this._state.page = nextPage;
54
+ changed = true;
55
+ }
56
+ }
57
+ if (meta.perPage !== undefined) {
58
+ const nextPerPage = this._normalizePerPage(meta.perPage, this._state.perPage);
59
+ if (nextPerPage !== this._state.perPage) {
60
+ this._state.perPage = nextPerPage;
61
+ changed = true;
62
+ }
63
+ }
64
+ if (meta.totalPages !== undefined) {
65
+ const nextTotalPages = this._normalizeInt(meta.totalPages, this._state.totalPages);
66
+ if (nextTotalPages !== this._state.totalPages) {
67
+ this._state.totalPages = nextTotalPages;
68
+ changed = true;
69
+ }
70
+ }
71
+ const clampedPage = this._clampPage(this._state.page);
72
+ if (clampedPage !== this._state.page) {
73
+ this._state.page = clampedPage;
74
+ changed = true;
75
+ }
76
+ if (!changed) {
77
+ return;
78
+ }
79
+ this._render();
80
+ }
81
+
82
+ _resolveContainers() {
83
+ const mode = this._resolveRenderMode();
84
+ const found = this._findMarkupContainers();
85
+ if (mode === 'markup') return found;
86
+ if (mode === 'auto' && found.length) return found;
87
+ return this._createClassContainers();
88
+ }
89
+
90
+ _resolveRenderMode() {
91
+ const render = (this._params.renderMode || this._params.render || 'auto').toString().toLowerCase();
92
+ if (render === 'class') return 'class';
93
+ if (render === 'markup') return 'markup';
94
+ return 'auto';
95
+ }
96
+
97
+ _findMarkupContainers() {
98
+ if (this._params.container instanceof Element) {
99
+ return [this._params.container];
100
+ }
101
+ if (typeof this._params.container === 'string' && this._params.container.trim()) {
102
+ const scopedContainer = this._root.querySelector(this._params.container);
103
+ if (scopedContainer) return [scopedContainer];
104
+ }
105
+
106
+ const list = Array.from(this._root.querySelectorAll(this._params.markupSelector || '.vgdt-pagination'));
107
+ if (!list.length) return [];
108
+
109
+ const position = (this._params.position || 'bottom').toString().toLowerCase();
110
+ if (position === 'both') return list;
111
+
112
+ const preferred = list.find((item) => item.dataset.position === position);
113
+ return preferred ? [preferred] : [list[0]];
114
+ }
115
+
116
+ _createClassContainers() {
117
+ const position = (this._params.position || 'bottom').toString().toLowerCase();
118
+ const table = this._root.querySelector('table');
119
+ const tableHost = table ? (table.closest('.vgdt-table-viewport') || table) : null;
120
+ const containers = [];
121
+
122
+ const ensure = (pos) => {
123
+ const className = `vgdt-pagination vgdt-pagination--${pos}`;
124
+ let container = this._root.querySelector(`.vgdt-pagination--${pos}[data-pagination-generated="1"]`);
125
+ if (!container) {
126
+ container = document.createElement('div');
127
+ container.className = className;
128
+ container.dataset.position = pos;
129
+ container.dataset.paginationGenerated = '1';
130
+ if (tableHost && tableHost.parentElement === this._root) {
131
+ if (pos === 'top') {
132
+ this._root.insertBefore(container, tableHost);
133
+ } else {
134
+ if (tableHost.nextSibling) {
135
+ this._root.insertBefore(container, tableHost.nextSibling);
136
+ } else {
137
+ this._root.appendChild(container);
138
+ }
139
+ }
140
+ } else {
141
+ this._root.appendChild(container);
142
+ }
143
+ }
144
+ containers.push(container);
145
+ };
146
+
147
+ if (position === 'top' || position === 'both') ensure('top');
148
+ if (position === 'bottom' || position === 'both') ensure('bottom');
149
+ return containers;
150
+ }
151
+
152
+ _bindEvents() {
153
+ this._containers.forEach((container) => {
154
+ container.removeEventListener('click', this._boundContainerClick);
155
+ container.removeEventListener('change', this._boundContainerChange);
156
+ container.removeEventListener('keydown', this._boundContainerKeyDown);
157
+ container.removeEventListener('focusin', this._boundContainerFocusIn);
158
+ container.removeEventListener('focusout', this._boundContainerBlur);
159
+ container.addEventListener('click', this._boundContainerClick);
160
+ container.addEventListener('change', this._boundContainerChange);
161
+ container.addEventListener('keydown', this._boundContainerKeyDown);
162
+ container.addEventListener('focusin', this._boundContainerFocusIn);
163
+ container.addEventListener('focusout', this._boundContainerBlur);
164
+ });
165
+ }
166
+
167
+ _handleContainerClick(event) {
168
+ const perPageOption = event.target.closest('[data-per-page-option]');
169
+ if (perPageOption) {
170
+ event.preventDefault();
171
+ const input = this._findPerPageInputFromNode(perPageOption);
172
+ const value = perPageOption.getAttribute('data-per-page-option') || '';
173
+ if (input) {
174
+ input.value = this._formatPerPageDisplay(value);
175
+ }
176
+ this._applyPerPage(value);
177
+ this._hidePerPageDropdownFromNode(perPageOption);
178
+ return;
179
+ }
180
+
181
+ const perPageInput = event.target.closest('[data-per-page]');
182
+ if (perPageInput) {
183
+ event.preventDefault();
184
+ event.stopPropagation();
185
+ const toggle = this._findPerPageToggleFromNode(perPageInput);
186
+ const dropdown = this._getOrCreatePerPageDropdown(toggle);
187
+ if (dropdown && typeof dropdown.toggle === 'function') {
188
+ dropdown.toggle();
189
+ } else if (dropdown && typeof dropdown.show === 'function') {
190
+ dropdown.show();
191
+ }
192
+ return;
193
+ }
194
+
195
+ const jumpButton = event.target.closest('[data-page-jump]');
196
+ if (jumpButton) {
197
+ const root = jumpButton.closest('.vgdt-pagination__jump');
198
+ const input = root ? root.querySelector('[data-page-jump-input]') : null;
199
+ this._jumpToPage(input ? input.value : '');
200
+ return;
201
+ }
202
+
203
+ const button = event.target.closest('[data-page]');
204
+ if (!button) return;
205
+
206
+ const page = button.dataset.page;
207
+ if (page === 'prev') {
208
+ this._setPage(this._state.page - 1, 'prev');
209
+ return;
210
+ }
211
+ if (page === 'next') {
212
+ this._setPage(this._state.page + 1, 'next');
213
+ return;
214
+ }
215
+ if (page === 'ellipsis-prev') {
216
+ this._setPage(this._getEllipsisTargetPage('prev'), 'ellipsis');
217
+ return;
218
+ }
219
+ if (page === 'ellipsis-next') {
220
+ this._setPage(this._getEllipsisTargetPage('next'), 'ellipsis');
221
+ return;
222
+ }
223
+ this._setPage(this._normalizeInt(page, this._state.page), 'page');
224
+ }
225
+
226
+ _handleContainerKeyDown(event) {
227
+ if (event.key === 'Escape') {
228
+ const perPageInput = event.target.closest('[data-per-page]');
229
+ if (perPageInput) {
230
+ this._hidePerPageDropdownFromNode(perPageInput);
231
+ perPageInput.blur();
232
+ return;
233
+ }
234
+ }
235
+
236
+ if (event.key !== 'Enter') return;
237
+
238
+ const perPageInput = event.target.closest('[data-per-page]');
239
+ if (perPageInput) {
240
+ event.preventDefault();
241
+ this._applyPerPage(perPageInput.value);
242
+ perPageInput.value = this._formatPerPageDisplay(this._state.perPage);
243
+ return;
244
+ }
245
+
246
+ const input = event.target.closest('[data-page-jump-input]');
247
+ if (!input) return;
248
+
249
+ event.preventDefault();
250
+ this._jumpToPage(input.value);
251
+ }
252
+
253
+ _handleContainerChange(event) {
254
+ const input = event.target.closest('[data-per-page]');
255
+ if (input) {
256
+ this._applyPerPage(input.value);
257
+ input.value = this._formatPerPageDisplay(this._state.perPage);
258
+ }
259
+ }
260
+
261
+ _handleContainerFocusIn(event) {
262
+ const input = event.target.closest('[data-per-page]');
263
+ if (!input) {
264
+ return;
265
+ }
266
+ input.value = String(this._normalizePerPage(input.value, this._state.perPage));
267
+ }
268
+
269
+ _handleContainerBlur(event) {
270
+ const input = event.target.closest('[data-per-page]');
271
+ if (!input) {
272
+ return;
273
+ }
274
+ const related = event.relatedTarget instanceof Element ? event.relatedTarget : null;
275
+ const control = input.closest('.vgdt-pagination__per-page-control');
276
+ if (control && related && control.contains(related)) {
277
+ return;
278
+ }
279
+ this._applyPerPage(input.value);
280
+ input.value = this._formatPerPageDisplay(this._state.perPage);
281
+ }
282
+
283
+ _applyPerPage(value) {
284
+ const perPage = this._normalizePerPage(value, this._state.perPage);
285
+ const perPageChanged = perPage !== this._state.perPage;
286
+ const pageChanged = this._state.page !== 1;
287
+ if (!perPageChanged && !pageChanged) return;
288
+
289
+ this._state.perPage = perPage;
290
+ this._state.page = 1;
291
+ this._render();
292
+ this._emitChange(perPageChanged, 'per-page');
293
+ }
294
+
295
+ _setPage(page, source = 'page') {
296
+ const normalized = this._clampPage(page);
297
+ if (normalized === this._state.page) return;
298
+ this._state.page = normalized;
299
+ this._render();
300
+ this._emitChange(false, source);
301
+ }
302
+
303
+ _emitChange(perPageChanged, source = 'page') {
304
+ const payload = {
305
+ page: this._state.page,
306
+ perPage: this._state.perPage,
307
+ totalPages: this._state.totalPages,
308
+ perPageChanged: Boolean(perPageChanged),
309
+ source,
310
+ };
311
+
312
+ const onChange = this._params.onChange;
313
+ if (typeof onChange === 'function') {
314
+ onChange(payload);
315
+ }
316
+ const onPerPageChange = this._params.onPerPageChange;
317
+ if (perPageChanged && typeof onPerPageChange === 'function') {
318
+ onPerPageChange(payload);
319
+ }
320
+
321
+ this._root.dispatchEvent(new CustomEvent('vgdt:pagination:change', {
322
+ bubbles: true,
323
+ detail: payload,
324
+ }));
325
+ }
326
+
327
+ _render() {
328
+ const pages = this._buildPages();
329
+ const markup = this._buildMarkup(pages);
330
+ this._containers.forEach((container) => {
331
+ const lastMarkup = this._lastMarkupByContainer.get(container) || '';
332
+ if (lastMarkup === markup) {
333
+ return;
334
+ }
335
+ container.innerHTML = markup;
336
+ this._lastMarkupByContainer.set(container, markup);
337
+ this._initPerPageDropdown(container);
338
+ });
339
+ }
340
+
341
+ _buildMarkup(pages) {
342
+ const align = this._resolveAlign();
343
+ const alignStyle = this._resolveAlignStyle(align);
344
+ const prevDisabled = this._state.page <= 1 ? 'disabled' : '';
345
+ const nextDisabled = this._state.page >= this._state.totalPages ? 'disabled' : '';
346
+ const showPerPage = this._params.showPerPage;
347
+ const perPageHtml = showPerPage ? this._buildPerPageMarkup() : '';
348
+ const quickJumpHtml = this._shouldShowQuickJump()
349
+ ? this._buildQuickJumpMarkup()
350
+ : '';
351
+
352
+ return `
353
+ <div class="vgdt-pagination__inner vgdt-pagination__inner--${align}" style="display:flex;align-items:center;justify-content:${alignStyle};gap:12px;flex-wrap:wrap;">
354
+ ${perPageHtml}
355
+ <nav class="vgdt-pagination__pages" aria-label="Pagination">
356
+ <button type="button" class="vgdt-page vgdt-page--prev" data-page="prev" ${prevDisabled}>${this._params.prevLabel}</button>
357
+ ${pages.map((item) => this._buildPageItem(item)).join('')}
358
+ <button type="button" class="vgdt-page vgdt-page--next" data-page="next" ${nextDisabled}>${this._params.nextLabel}</button>
359
+ </nav>
360
+ ${quickJumpHtml}
361
+ </div>
362
+ `;
363
+ }
364
+
365
+ _buildPerPageMarkup() {
366
+ const options = this._normalizePerPageOptions();
367
+ const showLabel = this._isPerPageLabelVisible();
368
+ const perPageLabelMarkup = showLabel ? `<span>${this._params.perPageLabel}</span>` : '';
369
+ return `
370
+ <label class="vgdt-pagination__per-page">
371
+ ${perPageLabelMarkup}
372
+ <div class="vgdt-pagination__per-page-control vg-dropdown">
373
+ <input
374
+ type="text"
375
+ min="1"
376
+ max="${this._maxPerPage}"
377
+ step="1"
378
+ inputmode="numeric"
379
+ data-per-page
380
+ data-toggle="dropdown"
381
+ value="${this._formatPerPageDisplay(this._state.perPage)}"
382
+ aria-label="${this._params.perPageLabel}"
383
+ aria-haspopup="true"
384
+ >
385
+ <span class="vgdt-pagination__per-page-chevron" aria-hidden="true">
386
+ <svg viewBox="0 0 20 20" focusable="false" aria-hidden="true">
387
+ <path d="M5 7.5L10 12.5L15 7.5" />
388
+ </svg>
389
+ </span>
390
+ <div class="vg-dropdown-content vgdt-pagination__per-page-dropdown">
391
+ <div class="vg-dropdown-container">
392
+ ${options.map((value) => {
393
+ return `<button type="button" class="vgdt-pagination__per-page-option" data-per-page-option="${value}">${this._formatPerPageDisplay(value)}</button>`;
394
+ }).join('')}
395
+ </div>
396
+ </div>
397
+ </div>
398
+ </label>
399
+ `;
400
+ }
401
+
402
+ _isPerPageLabelVisible() {
403
+ const table = this._root ? this._root.querySelector('table') : null;
404
+ const attr = table
405
+ ? table.getAttribute('data-pagination-show-per-page-label')
406
+ : null;
407
+ if (attr !== null) {
408
+ const normalized = String(attr).toLowerCase().trim();
409
+ return normalized !== 'false' && normalized !== '0';
410
+ }
411
+
412
+ return Boolean(this._params.showPerPageLabel);
413
+ }
414
+
415
+ _buildPageItem(item) {
416
+ if (item && typeof item === 'object' && item.type === 'ellipsis') {
417
+ const isPrev = item.direction === 'prev';
418
+ const pageAction = isPrev ? 'ellipsis-prev' : 'ellipsis-next';
419
+ const chevron = isPrev ? '&laquo;' : '&raquo;';
420
+ const targetPage = this._getEllipsisTargetPage(item.direction);
421
+ const jumpStep = Math.abs(targetPage - this._state.page);
422
+ const label = isPrev
423
+ ? `Go back ${jumpStep} page${jumpStep > 1 ? 's' : ''}`
424
+ : `Go forward ${jumpStep} page${jumpStep > 1 ? 's' : ''}`;
425
+ return `<button type="button" class="vgdt-page vgdt-page--ellipsis" data-page="${pageAction}" aria-label="${label}"><span class="vgdt-ellipsis-dots" aria-hidden="true">...</span><span class="vgdt-ellipsis-chevron" aria-hidden="true">${chevron}</span></button>`;
426
+ }
427
+ const active = item === this._state.page ? 'is-active' : '';
428
+ return `<button type="button" class="vgdt-page ${active}" data-page="${item}" aria-current="${active ? 'page' : 'false'}">${item}</button>`;
429
+ }
430
+
431
+ _buildQuickJumpMarkup() {
432
+ return `
433
+ <div class="vgdt-pagination__jump">
434
+ <input
435
+ type="number"
436
+ min="1"
437
+ max="${this._state.totalPages}"
438
+ step="1"
439
+ inputmode="numeric"
440
+ data-page-jump-input
441
+ value="${this._state.page}"
442
+ >
443
+ <button type="button" class="vgdt-page vgdt-page--jump" data-page-jump>${this._params.quickJumpButtonLabel || 'Go'}</button>
444
+ </div>
445
+ `;
446
+ }
447
+
448
+ _buildPages() {
449
+ const total = this._state.totalPages;
450
+ const current = this._clampPage(this._state.page);
451
+ const maxVisible = Math.max(1, this._normalizeInt(this._params.maxVisiblePages, 5));
452
+ const shouldEllipsis = Boolean(this._params.ellipsis) && total > this._normalizeInt(this._params.ellipsisAfter, 5);
453
+
454
+ if (!shouldEllipsis || total <= maxVisible + 2) {
455
+ return this._range(1, total);
456
+ }
457
+
458
+ const middleSlots = maxVisible;
459
+ let start = Math.max(2, current - Math.floor(middleSlots / 2));
460
+ let end = Math.min(total - 1, start + middleSlots - 1);
461
+
462
+ start = Math.max(2, end - middleSlots + 1);
463
+
464
+ const pages = [1];
465
+ if (start > 2) pages.push({ type: 'ellipsis', direction: 'prev' });
466
+ for (let page = start; page <= end; page += 1) {
467
+ pages.push(page);
468
+ }
469
+ if (end < total - 1) pages.push({ type: 'ellipsis', direction: 'next' });
470
+ pages.push(total);
471
+ return pages;
472
+ }
473
+
474
+ _findPerPageInputFromNode(node) {
475
+ const root = node ? node.closest('.vgdt-pagination__per-page-control') : null;
476
+ return root ? root.querySelector('[data-per-page]') : null;
477
+ }
478
+
479
+ _findPerPageToggleFromNode(node) {
480
+ const root = node ? node.closest('.vgdt-pagination__per-page-control') : null;
481
+ return root ? root.querySelector('[data-per-page][data-toggle="dropdown"]') : null;
482
+ }
483
+
484
+ _getOrCreatePerPageDropdown(toggle) {
485
+ if (!toggle) {
486
+ return null;
487
+ }
488
+ let instance = this._dropdownInstances.get(toggle);
489
+ if (instance) {
490
+ return instance;
491
+ }
492
+ instance = VGDropdown.getOrCreateInstance(toggle, {
493
+ placement: 'auto',
494
+ hover: false,
495
+ animation: {
496
+ fade: true,
497
+ enable: false,
498
+ delay: 120,
499
+ },
500
+ });
501
+ this._dropdownInstances.set(toggle, instance);
502
+ return instance;
503
+ }
504
+
505
+ _initPerPageDropdown(container) {
506
+ const toggle = container.querySelector('.vgdt-pagination__per-page-control [data-toggle="dropdown"]');
507
+ if (!toggle) {
508
+ return;
509
+ }
510
+ this._getOrCreatePerPageDropdown(toggle);
511
+ }
512
+
513
+ _hidePerPageDropdownFromNode(node) {
514
+ const toggle = this._findPerPageToggleFromNode(node);
515
+ if (!toggle) {
516
+ return;
517
+ }
518
+ const dropdown = this._dropdownInstances.get(toggle);
519
+ if (dropdown && typeof dropdown.hide === 'function') {
520
+ dropdown.hide();
521
+ }
522
+ }
523
+
524
+ _shouldShowQuickJump() {
525
+ if (this._state.totalPages <= 1) return false;
526
+
527
+ const mode = this._params.quickJump;
528
+ if (mode === true || mode === 'true') return true;
529
+ if (mode === false || mode === 'false') return false;
530
+
531
+ const threshold = this._normalizeInt(this._params.ellipsisAfter, 5);
532
+ return this._state.totalPages > threshold;
533
+ }
534
+
535
+ _resolveAlign() {
536
+ const value = (this._params.align || 'left').toString().toLowerCase();
537
+ if (value === 'center' || value === 'right' || value === 'between') return value;
538
+ return 'left';
539
+ }
540
+
541
+ _resolveAlignStyle(align) {
542
+ if (align === 'center') return 'center';
543
+ if (align === 'right') return 'flex-end';
544
+ if (align === 'between') return 'space-between';
545
+ return 'flex-start';
546
+ }
547
+
548
+ _normalizePerPageOptions() {
549
+ const list = this._params.perPageOptions;
550
+ const options = Array.isArray(list) ? list : [10, 25, 50, 100];
551
+ const normalized = options
552
+ .map((item) => this._normalizeInt(item, 0))
553
+ .filter((item) => item > 0 && item <= this._maxPerPage);
554
+ if (!normalized.length) return [this._state.perPage];
555
+ if (!normalized.includes(this._state.perPage)) normalized.push(this._state.perPage);
556
+ return Array.from(new Set(normalized)).sort((a, b) => a - b);
557
+ }
558
+
559
+ _resolveMaxPerPage() {
560
+ const raw = this._params.maxPerPage;
561
+ return this._normalizeInt(raw, 100);
562
+ }
563
+
564
+ _normalizePerPage(value, fallback) {
565
+ const normalized = this._normalizeInt(value, fallback);
566
+ return Math.min(normalized, this._maxPerPage);
567
+ }
568
+
569
+ _getPerPageOptionSuffix() {
570
+ const rawSuffix = this._params.perPageOptionSuffix;
571
+ const normalized = String(rawSuffix || '').trim();
572
+ if (normalized) {
573
+ return normalized;
574
+ }
575
+ const label = String(this._params.perPageLabel || '').toLowerCase();
576
+ return /[а-яё]/i.test(label) ? 'на страницу' : 'page';
577
+ }
578
+
579
+ _formatPerPageDisplay(value) {
580
+ const normalizedValue = this._normalizeInt(value, this._state.perPage);
581
+ return `${normalizedValue} / ${this._getPerPageOptionSuffix()}`;
582
+ }
583
+
584
+ _clampPage(page) {
585
+ const normalized = this._normalizeInt(page, 1);
586
+ return Math.max(1, Math.min(normalized, this._state.totalPages));
587
+ }
588
+
589
+ _normalizeInt(value, fallback) {
590
+ const parsed = Number.parseInt(value, 10);
591
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
592
+ }
593
+
594
+ _jumpToPage(value) {
595
+ const page = this._normalizeInt(value, this._state.page);
596
+ this._setPage(page, 'jump');
597
+ }
598
+
599
+ _getEllipsisTargetPage(direction) {
600
+ const pages = this._buildPages();
601
+ const visibleNumericPages = pages.filter((item) => Number.isInteger(item) && item > 1 && item < this._state.totalPages);
602
+ if (!visibleNumericPages.length) {
603
+ return direction === 'prev' ? 1 : this._state.totalPages;
604
+ }
605
+ const firstVisible = visibleNumericPages[0];
606
+ const lastVisible = visibleNumericPages[visibleNumericPages.length - 1];
607
+ if (direction === 'prev') {
608
+ return this._clampPage(firstVisible - 1);
609
+ }
610
+ return this._clampPage(lastVisible + 1);
611
+ }
612
+
613
+ _range(start, end) {
614
+ const result = [];
615
+ for (let value = start; value <= end; value += 1) {
616
+ result.push(value);
617
+ }
618
+ return result;
619
+ }
620
+ }
621
+
622
+ export default Pagination
623
+
@@ -0,0 +1,82 @@
1
+ import {normalizeNonNegativeInt, safeQuerySelector} from "./utils/common";
2
+
3
+ class Search {
4
+ constructor(options = {}) {
5
+ this._options = Object.assign({
6
+ enabled: true,
7
+ debounceMs: 300,
8
+ inputSelector: '',
9
+ param: 'q',
10
+ onSearch: null,
11
+ }, options);
12
+
13
+ this._input = null;
14
+ this._timer = null;
15
+
16
+ this._boundInput = this._handleInput.bind(this);
17
+ this._boundKeyDown = this._handleKeyDown.bind(this);
18
+ }
19
+
20
+ init() {
21
+ if (!this._options.enabled) {
22
+ return;
23
+ }
24
+
25
+ const selector = String(this._options.inputSelector || '').trim();
26
+ if (!selector) {
27
+ return;
28
+ }
29
+
30
+ const input = safeQuerySelector(selector);
31
+ if (!input) {
32
+ return;
33
+ }
34
+
35
+ this._input = input;
36
+ this._input.addEventListener('input', this._boundInput);
37
+ this._input.addEventListener('keydown', this._boundKeyDown);
38
+ }
39
+
40
+ destroy() {
41
+ if (!this._input) {
42
+ return;
43
+ }
44
+
45
+ this._input.removeEventListener('input', this._boundInput);
46
+ this._input.removeEventListener('keydown', this._boundKeyDown);
47
+ this._input = null;
48
+ clearTimeout(this._timer);
49
+ this._timer = null;
50
+ }
51
+
52
+ _handleInput() {
53
+ clearTimeout(this._timer);
54
+ this._timer = setTimeout(() => {
55
+ this._emit();
56
+ }, normalizeNonNegativeInt(this._options.debounceMs, 300));
57
+ }
58
+
59
+ _handleKeyDown(event) {
60
+ if (event.key !== 'Enter') {
61
+ return;
62
+ }
63
+
64
+ event.preventDefault();
65
+ clearTimeout(this._timer);
66
+ this._emit();
67
+ }
68
+
69
+ _emit() {
70
+ if (typeof this._options.onSearch !== 'function' || !this._input) {
71
+ return;
72
+ }
73
+
74
+ this._options.onSearch({
75
+ value: this._input.value.trim(),
76
+ param: String(this._options.param || 'q'),
77
+ });
78
+ }
79
+ }
80
+
81
+ export default Search;
82
+