@kamranbaylarov/one-select 1.1.0 → 1.2.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.
package/js/one-select.js CHANGED
@@ -1,1186 +1,1412 @@
1
- /**
2
- * OneSelect - jQuery Multi-Select Dropdown Plugin
3
- * Version: 1.0.0
4
- * https://github.com/your-repo/one-select
5
- *
6
- * Copyright 2024
7
- * Licensed under MIT
8
- *
9
- * A powerful, flexible, and feature-rich multi-select dropdown component for jQuery.
10
- */
11
-
12
- (function(factory) {
13
- 'use strict';
14
-
15
- if (typeof define === 'function' && define.amd) {
16
- // AMD
17
- define(['jquery'], factory);
18
- } else if (typeof module === 'object' && module.exports) {
19
- // CommonJS
20
- module.exports = function(root, jQuery) {
21
- if (jQuery === undefined) {
22
- if (typeof window !== 'undefined') {
23
- jQuery = require('jquery');
24
- } else {
25
- jQuery = require('jquery')(root);
26
- }
27
- }
28
- factory(jQuery);
29
- return jQuery;
30
- };
31
- } else {
32
- // Browser globals
33
- factory(window.jQuery || window.$);
34
- }
35
-
36
- }(function($) {
37
- 'use strict';
38
-
39
- // Global registry for all instances
40
- var instances = {};
41
- var pluginName = 'oneSelect';
42
-
43
- /**
44
- * Debounce utility function
45
- * @param {Function} func - Function to debounce
46
- * @param {Number} delay - Delay in milliseconds
47
- * @returns {Function} Debounced function
48
- */
49
- function debounce(func, delay) {
50
- var timeoutId;
51
- return function() {
52
- var context = this;
53
- var args = arguments;
54
- clearTimeout(timeoutId);
55
- timeoutId = setTimeout(function() {
56
- func.apply(context, args);
57
- }, delay);
58
- };
59
- }
60
-
61
- /**
62
- * OneSelect Constructor
63
- * @param {HTMLElement} element - The DOM element
64
- * @param {Object} options - Configuration options
65
- */
66
- var OneSelect = function(element, options) {
67
- this.element = element;
68
- this.$element = $(element);
69
-
70
- // Generate unique instance ID for control
71
- this.instanceId = 'ones-' + Math.random().toString(36).substr(2, 9);
72
-
73
- // Read data attributes from element (highest priority)
74
- var dataOptions = this.readDataAttributes();
75
-
76
- // Merge: defaults -> options (JS) -> dataOptions (HTML attributes)
77
- this.settings = $.extend({}, OneSelect.defaults, options, dataOptions);
78
- this.init();
79
- };
80
-
81
- /**
82
- * Default configuration options
83
- */
84
- OneSelect.defaults = {
85
- placeholder: 'Select options...',
86
- selectAllText: 'Select All',
87
- okText: 'OK',
88
- cancelText: 'Cancel',
89
- data: [],
90
- value: null, // Single value or array of values to pre-select (index-based)
91
- showCheckbox: true,
92
- showBadges: false,
93
- showBadgesExternal: null,
94
- showSearch: false, // Show search input in dropdown
95
- searchPlaceholder: 'Search...',
96
- searchUrl: null, // URL for AJAX search (GET request)
97
- searchDebounceDelay: 300,// Delay in milliseconds for search debounce
98
- closeOnScroll: false,
99
- closeOnOutside: true, // Close dropdown when clicking outside (default: true)
100
- submitForm: false,
101
- submitOnOutside: false,
102
- formId: null,
103
- name: null,
104
- multiple: true,
105
- ajax: null,
106
- autoLoad: true,
107
- beforeLoad: null,
108
- afterLoad: null,
109
- onLoadError: null,
110
- onChange: null,
111
- onSelect: null,
112
- onOk: null,
113
- onCancel: null
114
- };
115
-
116
- /**
117
- * OneSelect Prototype
118
- */
119
- OneSelect.prototype = {
120
- /**
121
- * Read data attributes from HTML element
122
- */
123
- readDataAttributes: function() {
124
- var self = this;
125
- var dataOptions = {};
126
-
127
- var attributeMap = {
128
- 'ones-placeholder': 'placeholder',
129
- 'ones-select-all-text': 'selectAllText',
130
- 'ones-ok-text': 'okText',
131
- 'ones-cancel-text': 'cancelText',
132
- 'ones-data': 'data',
133
- 'ones-value': 'value',
134
- 'ones-name': 'name',
135
- 'ones-multiple': 'multiple',
136
- 'ones-show-checkbox': 'showCheckbox',
137
- 'ones-show-badges': 'showBadges',
138
- 'ones-show-badges-external': 'showBadgesExternal',
139
- 'ones-show-search': 'showSearch',
140
- 'ones-search-placeholder': 'searchPlaceholder',
141
- 'ones-search-url': 'searchUrl',
142
- 'ones-search-debounce-delay': 'searchDebounceDelay',
143
- 'ones-close-on-scroll': 'closeOnScroll',
144
- 'ones-close-on-outside': 'closeOnOutside',
145
- 'ones-submit-form': 'submitForm',
146
- 'ones-submit-on-outside': 'submitOnOutside',
147
- 'ones-form-id': 'formId',
148
- 'ones-auto-load': 'autoLoad'
149
- };
150
-
151
- $.each(attributeMap, function(attr, setting) {
152
- var value = self.$element.data(attr);
153
-
154
- if (value === undefined) {
155
- return;
156
- }
157
-
158
- if (setting === 'data' || setting === 'value') {
159
- if (typeof value === 'string') {
160
- try {
161
- dataOptions[setting] = JSON.parse(value);
162
- } catch (e) {
163
- console.warn('OneSelect: Invalid JSON for ' + attr, value);
164
- dataOptions[setting] = value;
165
- }
166
- } else {
167
- dataOptions[setting] = value;
168
- }
169
- } else if (setting === 'multiple' || setting === 'showCheckbox' ||
170
- setting === 'showBadges' || setting === 'showSearch' ||
171
- setting === 'closeOnScroll' || setting === 'closeOnOutside' ||
172
- setting === 'submitForm' || setting === 'submitOnOutside' ||
173
- setting === 'autoLoad') {
174
- if (typeof value === 'string') {
175
- dataOptions[setting] = value === 'true' || value === '1';
176
- } else {
177
- dataOptions[setting] = !!value;
178
- }
179
- } else if (setting === 'searchDebounceDelay') {
180
- // Parse as number
181
- if (typeof value === 'string') {
182
- dataOptions[setting] = parseInt(value, 10) || 300;
183
- } else {
184
- dataOptions[setting] = value || 300;
185
- }
186
- } else {
187
- dataOptions[setting] = value;
188
- }
189
- });
190
-
191
- var ajaxData = this.$element.data('ones-ajax');
192
- if (ajaxData) {
193
- if (typeof ajaxData === 'string') {
194
- try {
195
- dataOptions.ajax = JSON.parse(ajaxData);
196
- } catch (e) {
197
- console.warn('OneSelect: Invalid JSON for ones-ajax', ajaxData);
198
- }
199
- } else {
200
- dataOptions.ajax = ajaxData;
201
- }
202
- }
203
-
204
- return dataOptions;
205
- },
206
-
207
- init: function() {
208
- // Register instance in global registry
209
- instances[this.instanceId] = this;
210
-
211
- // Convert value to array if needed
212
- if (this.settings.value !== null && this.settings.value !== undefined) {
213
- if (!Array.isArray(this.settings.value)) {
214
- this.settings.value = [this.settings.value];
215
- }
216
- } else {
217
- this.settings.value = [];
218
- }
219
-
220
- this.wrapper = this.createWrapper();
221
- this.trigger = this.createTrigger();
222
- this.dropdown = this.createDropdown();
223
- this.searchInput = this.createSearchInput();
224
- this.optionsContainer = this.createOptionsContainer();
225
- this.buttons = this.createButtons();
226
-
227
- this.build();
228
- this.attachEvents();
229
-
230
- if (this.settings.ajax && this.settings.autoLoad) {
231
- this.loadData();
232
- }
233
- },
234
-
235
- build: function() {
236
- // Add search input at the top of dropdown if enabled
237
- if (this.settings.showSearch) {
238
- this.dropdown.append(this.searchInput);
239
- }
240
- this.dropdown.append(this.optionsContainer);
241
- this.dropdown.append(this.buttons);
242
- this.wrapper.append(this.trigger);
243
-
244
- // Append wrapper to $element, dropdown to body
245
- this.$element.append(this.wrapper);
246
- $('body').append(this.dropdown);
247
-
248
- this.renderOptions();
249
- this.updateTriggerText();
250
- this.updateHiddenInputs();
251
- },
252
-
253
- updateHiddenInputs: function() {
254
- if (!this.settings.name) {
255
- return;
256
- }
257
-
258
- var form = null;
259
- if (this.settings.formId) {
260
- form = $('#' + this.settings.formId);
261
- } else {
262
- form = this.$element.closest('form');
263
- }
264
-
265
- var container = form.length ? form : this.wrapper;
266
-
267
- container.find('input.cms-hidden-input[data-cms-input="' + this.settings.name + '"]').remove();
268
-
269
- var inputName = this.settings.name;
270
- if (this.settings.multiple && inputName.indexOf('[') === -1) {
271
- inputName += '[]';
272
- }
273
-
274
- var selectedValues = this.getSelectedValues();
275
-
276
- if (this.settings.multiple) {
277
- $.each(selectedValues, function(index, value) {
278
- var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
279
- .attr('name', inputName)
280
- .attr('value', value)
281
- .attr('data-cms-input', this.settings.name)
282
- .attr('data-cms-value', value);
283
- container.append(hiddenInput);
284
- }.bind(this));
285
- } else {
286
- var value = selectedValues.length > 0 ? selectedValues.join(',') : '';
287
- var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
288
- .attr('name', inputName)
289
- .attr('value', value)
290
- .attr('data-cms-input', this.settings.name);
291
- container.append(hiddenInput);
292
- }
293
- },
294
-
295
- createWrapper: function() {
296
- return $('<div class="cms-wrapper"></div>');
297
- },
298
-
299
- createTrigger: function() {
300
- return $('<div class="cms-trigger"><span class="cms-selected-text cms-placeholder">' +
301
- this.settings.placeholder + '</span></div>');
302
- },
303
-
304
- createDropdown: function() {
305
- return $('<div class="cms-dropdown"></div>');
306
- },
307
-
308
- createSearchInput: function() {
309
- return $('<div class="cms-search-wrapper">' +
310
- '<input type="text" class="cms-search-input" placeholder="' +
311
- this.settings.searchPlaceholder + '" /></div>');
312
- },
313
-
314
- createOptionsContainer: function() {
315
- return $('<div class="cms-options-container"></div>');
316
- },
317
-
318
- createButtons: function() {
319
- var container = $('<div class="cms-buttons"></div>');
320
- this.okBtn = $('<button class="cms-btn cms-btn-ok">' + this.settings.okText + '</button>');
321
- this.cancelBtn = $('<button class="cms-btn cms-btn-cancel">' + this.settings.cancelText + '</button>');
322
-
323
- container.append(this.okBtn);
324
- container.append(this.cancelBtn);
325
-
326
- return container;
327
- },
328
-
329
- renderOptions: function() {
330
- this.optionsContainer.empty();
331
-
332
- var selectAllOption = this.createOption('select-all', this.settings.selectAllText, false);
333
- this.optionsContainer.append(selectAllOption);
334
-
335
- var self = this;
336
- $.each(this.settings.data, function(index, item) {
337
- // Always: value = index, label = item
338
- var value = index;
339
- var label = item;
340
-
341
- var isSelected = $.inArray(value, self.settings.value) !== -1;
342
- var option = self.createOption(value, label, isSelected);
343
- self.optionsContainer.append(option);
344
- });
345
-
346
- this.updateSelectAllState();
347
- },
348
-
349
- createOption: function(value, label, checked) {
350
- var optionClass = 'cms-option';
351
- if (!this.settings.showCheckbox) {
352
- optionClass += ' cms-hide-checkbox';
353
- }
354
-
355
- if (checked && value !== 'select-all') {
356
- optionClass += ' selected';
357
- }
358
-
359
- var option = $('<div class="' + optionClass + '" data-value="' + value + '"></div>');
360
- var checkbox = $('<input type="checkbox" value="' + value + '"' +
361
- (checked ? ' checked' : '') + '>');
362
- var labelEl = $('<label>' + label + '</label>');
363
-
364
- option.append(checkbox);
365
- option.append(labelEl);
366
-
367
- return option;
368
- },
369
-
370
- /**
371
- * Filter options based on search text
372
- * @param {String} searchText - Search text to filter by
373
- */
374
- filterOptions: function(searchText) {
375
- var self = this;
376
- var options = this.optionsContainer.find('.cms-option:not([data-value="select-all"])');
377
-
378
- if (searchText === '') {
379
- // Show all options if search is empty
380
- options.show();
381
- } else {
382
- // Filter options by label
383
- options.each(function() {
384
- var option = $(this);
385
- var label = option.find('label').text().toLowerCase();
386
-
387
- if (label.indexOf(searchText) !== -1) {
388
- option.show();
389
- } else {
390
- option.hide();
391
- }
392
- });
393
- }
394
- },
395
-
396
- /**
397
- * Perform AJAX search
398
- * @param {String} searchText - Search text to send to server
399
- */
400
- performAjaxSearch: function(searchText) {
401
- var self = this;
402
-
403
- // Show loading state
404
- this.optionsContainer.addClass('cms-loading');
405
-
406
- $.ajax({
407
- url: this.settings.searchUrl,
408
- method: 'GET',
409
- data: { q: searchText },
410
- dataType: 'json',
411
- success: function(response) {
412
- // Handle different response formats
413
- var data = response;
414
- if (typeof response === 'object' && response.data) {
415
- data = response.data;
416
- } else if (typeof response === 'object' && response.results) {
417
- data = response.results;
418
- }
419
-
420
- // Update dropdown with search results
421
- self.updateSearchResults(data || []);
422
-
423
- self.optionsContainer.removeClass('cms-loading');
424
- },
425
- error: function(xhr, status, error) {
426
- console.error('OneSelect: Search error', error);
427
- self.optionsContainer.removeClass('cms-loading');
428
-
429
- if (self.settings.onLoadError) {
430
- self.settings.onLoadError.call(self, xhr, status, error);
431
- }
432
- }
433
- });
434
- },
435
-
436
- /**
437
- * Update dropdown with search results
438
- * @param {Array} data - Search results data
439
- */
440
- updateSearchResults: function(data) {
441
- // Clear existing options (except select-all)
442
- this.optionsContainer.find('.cms-option:not([data-value="select-all"])').remove();
443
-
444
- var self = this;
445
- $.each(data, function(index, item) {
446
- // Always: value = index, label = item
447
- var value = index;
448
- var label = item;
449
-
450
- var isSelected = $.inArray(value, self.settings.value) !== -1;
451
- var option = self.createOption(value, label, isSelected);
452
- self.optionsContainer.append(option);
453
- });
454
-
455
- // Update select-all state
456
- this.updateSelectAllState();
457
- },
458
-
459
- attachEvents: function() {
460
- var self = this;
461
-
462
- this.trigger.on('click', function(e) {
463
- e.stopPropagation();
464
- self.toggle();
465
- });
466
-
467
- // Search input event listener
468
- if (this.settings.showSearch) {
469
- if (this.settings.searchUrl) {
470
- // AJAX search with debounce
471
- var debouncedSearch = debounce(function(searchText) {
472
- self.performAjaxSearch(searchText);
473
- }, this.settings.searchDebounceDelay);
474
-
475
- this.searchInput.find('.cms-search-input').on('keyup', function() {
476
- var searchText = $(this).val();
477
- if (searchText.length > 0) {
478
- debouncedSearch(searchText);
479
- } else {
480
- // Show original data when search is empty
481
- self.filterOptions('');
482
- }
483
- });
484
- } else {
485
- // Local filtering (default)
486
- this.searchInput.find('.cms-search-input').on('keyup', function() {
487
- var searchText = $(this).val().toLowerCase();
488
- self.filterOptions(searchText);
489
- });
490
- }
491
- }
492
-
493
- $(window).on('resize.cms', function() {
494
- if (self.wrapper.hasClass('open')) {
495
- self.updateDropdownPosition();
496
- }
497
- });
498
-
499
- if (this.settings.closeOnScroll) {
500
- $(window).on('scroll.cms', function() {
501
- if (self.wrapper.hasClass('open')) {
502
- self.close();
503
- }
504
- });
505
- } else {
506
- // Update dropdown position on vertical scroll
507
- $(window).on('scroll.cms', function() {
508
- if (self.wrapper.hasClass('open')) {
509
- self.updateDropdownPosition();
510
- }
511
- });
512
- }
513
-
514
- // Global horizontal scroll handler - close dropdown on any horizontal scroll
515
- // Listen for wheel events with horizontal delta
516
- $(document).on('wheel.onescroll', function(e) {
517
- if (!self.wrapper.hasClass('open')) {
518
- return;
519
- }
520
-
521
- // Check if horizontal scrolling (deltaX != 0)
522
- if (e.originalEvent && Math.abs(e.originalEvent.deltaX) > 0) {
523
- self.close();
524
- }
525
- });
526
-
527
- // Also listen for scroll events on all elements to detect horizontal scroll
528
- // Using MutationObserver to detect when elements with overflow scroll
529
- self._detectHorizontalScroll = function() {
530
- if (!self.wrapper.hasClass('open')) return;
531
-
532
- // Check window horizontal scroll
533
- if (window.scrollX !== self._lastWindowScrollX) {
534
- self._lastWindowScrollX = window.scrollX;
535
- if (self._lastWindowScrollX > 0) {
536
- self.close();
537
- return;
538
- }
539
- }
540
-
541
- // Check all scrollable elements for horizontal scroll
542
- var scrollableElements = document.querySelectorAll('*');
543
- for (var i = 0; i < scrollableElements.length; i++) {
544
- var el = scrollableElements[i];
545
- var key = getElementKey(el);
546
-
547
- if (self._elementScrollPositions[key] !== undefined) {
548
- var currentScroll = el.scrollLeft;
549
- if (currentScroll !== self._elementScrollPositions[key]) {
550
- // Horizontal scroll detected
551
- self.close();
552
- return;
553
- }
554
- }
555
- }
556
- };
557
-
558
- // Store scroll positions and track changes
559
- self._elementScrollPositions = {};
560
- self._lastWindowScrollX = window.scrollX;
561
-
562
- function getElementKey(el) {
563
- if (el === document) return 'document';
564
- if (el === document.documentElement) return 'html';
565
- if (el === document.body) return 'body';
566
- return el.tagName + '-' + (el.id || el.className || Math.random().toString(36).substr(2, 9));
567
- }
568
-
569
- // Override open to initialize tracking
570
- var originalOpen = self.open.bind(self);
571
- self.open = function() {
572
- // Store initial scroll positions
573
- self._elementScrollPositions = {};
574
- self._lastWindowScrollX = window.scrollX;
575
-
576
- var scrollableElements = document.querySelectorAll('*');
577
- for (var i = 0; i < scrollableElements.length; i++) {
578
- var el = scrollableElements[i];
579
- if (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) {
580
- self._elementScrollPositions[getElementKey(el)] = el.scrollLeft;
581
- }
582
- }
583
-
584
- // Start checking periodically
585
- if (self._horizontalScrollInterval) {
586
- clearInterval(self._horizontalScrollInterval);
587
- }
588
- self._horizontalScrollInterval = setInterval(function() {
589
- self._detectHorizontalScroll();
590
- }, 50);
591
-
592
- originalOpen();
593
- };
594
-
595
- // Override close to stop tracking
596
- var originalClose = self.close.bind(self);
597
- self.close = function() {
598
- if (self._horizontalScrollInterval) {
599
- clearInterval(self._horizontalScrollInterval);
600
- self._horizontalScrollInterval = null;
601
- }
602
- self._elementScrollPositions = {};
603
- originalClose();
604
- };
605
-
606
- // Window click handler - close dropdown when clicking outside
607
- $(window).on('click.ones', function(e) {
608
- if (!self.settings.closeOnOutside) {
609
- return;
610
- }
611
-
612
- if (!self.wrapper.hasClass('open')) {
613
- return;
614
- }
615
-
616
- var $target = $(e.target);
617
- // Don't close if clicked inside dropdown or wrapper elements
618
- if ($target.closest('.cms-wrapper').length > 0 ||
619
- $target.closest('.cms-dropdown').length > 0) {
620
- return;
621
- }
622
-
623
- // Submit form if submitOnOutside is enabled
624
- if (self.settings.submitOnOutside) {
625
- self.updateTriggerText();
626
- if (self.settings.onOk) {
627
- self.settings.onOk.call(self, self.getSelectedValues(), self.getSelectedLabels());
628
- }
629
- self.submitForm();
630
- }
631
-
632
- self.close();
633
- });
634
-
635
- this.optionsContainer.on('click', '.cms-option', function(e) {
636
- e.stopPropagation();
637
-
638
- var option = $(this);
639
- var checkbox = option.find('input[type="checkbox"]');
640
-
641
- if ($(e.target).is('input[type="checkbox"]')) {
642
- return;
643
- }
644
-
645
- checkbox.prop('checked', !checkbox.prop('checked'));
646
- self.handleOptionChange(option);
647
- });
648
-
649
- this.optionsContainer.on('change', 'input[type="checkbox"]', function(e) {
650
- e.stopPropagation();
651
- var option = $(e.target).closest('.cms-option');
652
- self.handleOptionChange(option);
653
- });
654
-
655
- this.okBtn.on('click', function(e) {
656
- e.stopPropagation();
657
- self.handleOk();
658
- });
659
-
660
- this.cancelBtn.on('click', function(e) {
661
- e.stopPropagation();
662
- self.handleCancel();
663
- });
664
- },
665
-
666
- handleOptionChange: function(option) {
667
- var value = option.data('value');
668
-
669
- if (value === 'select-all') {
670
- var checkbox = option.find('input[type="checkbox"]');
671
- this.handleSelectAll(checkbox.prop('checked'));
672
- } else {
673
- var checkbox = option.find('input[type="checkbox"]');
674
- if (checkbox.prop('checked')) {
675
- option.addClass('selected');
676
- } else {
677
- option.removeClass('selected');
678
- }
679
-
680
- var self = this;
681
- setTimeout(function() {
682
- self.updateSelectAllState();
683
- self.updateTriggerText();
684
- }, 0);
685
- }
686
-
687
- this.updateHiddenInputs();
688
-
689
- if (this.settings.onChange) {
690
- this.settings.onChange.call(this, this.getSelectedValues(), this.getSelectedLabels());
691
- }
692
-
693
- if (this.settings.onSelect) {
694
- this.settings.onSelect.call(this, this.getSelectedValues());
695
- }
696
- },
697
-
698
- handleSelectAll: function(checked) {
699
- var self = this;
700
- this.optionsContainer.find('.cms-option:not([data-value="select-all"])').each(function() {
701
- var option = $(this);
702
- option.find('input[type="checkbox"]').prop('checked', checked);
703
- if (checked) {
704
- option.addClass('selected');
705
- } else {
706
- option.removeClass('selected');
707
- }
708
- });
709
-
710
- this.updateSelectAllState();
711
- this.updateTriggerText();
712
- this.updateHiddenInputs();
713
- },
714
-
715
- updateSelectAllState: function() {
716
- var allOptions = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]');
717
- var checkedOptions = allOptions.filter(':checked');
718
- var totalCount = allOptions.length;
719
- var checkedCount = checkedOptions.length;
720
-
721
- var selectAllCheckbox = this.optionsContainer.find('.cms-option[data-value="select-all"] input[type="checkbox"]');
722
-
723
- selectAllCheckbox.prop('indeterminate', false);
724
- selectAllCheckbox.prop('checked', false);
725
-
726
- if (checkedCount === 0) {
727
- // Nothing selected
728
- } else if (checkedCount === totalCount && totalCount > 0) {
729
- selectAllCheckbox.prop('checked', true);
730
- } else {
731
- selectAllCheckbox.prop('indeterminate', true);
732
- }
733
- },
734
-
735
- getSelectedValues: function() {
736
- var values = [];
737
- this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]:checked')
738
- .each(function() {
739
- var val = $(this).val();
740
- if (!isNaN(val) && val !== '') {
741
- val = Number(val);
742
- }
743
- values.push(val);
744
- });
745
- return values;
746
- },
747
-
748
- getSelectedLabels: function() {
749
- var labels = [];
750
- this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]:checked')
751
- .siblings('label')
752
- .each(function() {
753
- labels.push($(this).text());
754
- });
755
- return labels;
756
- },
757
-
758
- updateTriggerText: function() {
759
- var labels = this.getSelectedLabels();
760
- var values = this.getSelectedValues();
761
- var textSpan = this.trigger.find('.cms-selected-text');
762
-
763
- if (labels.length === 0) {
764
- textSpan.empty().text(this.settings.placeholder).addClass('cms-placeholder');
765
- } else if (this.settings.showBadges) {
766
- textSpan.empty().removeClass('cms-placeholder');
767
-
768
- var self = this;
769
- $.each(values, function(index, value) {
770
- var badge = $('<span class="cms-badge"></span>');
771
- var labelSpan = $('<span></span>').text(labels[index]);
772
- var removeBtn = $('<button type="button" class="cms-badge-remove">&times;</button>');
773
-
774
- removeBtn.on('click', function(e) {
775
- e.stopPropagation();
776
- self.unselect(value);
777
- });
778
-
779
- badge.append(labelSpan);
780
- badge.append(removeBtn);
781
- textSpan.append(badge);
782
- });
783
- } else {
784
- textSpan.empty().removeClass('cms-placeholder');
785
- textSpan.text(labels.length + ' items selected');
786
- }
787
-
788
- this.updateExternalBadges(values, labels);
789
- },
790
-
791
- updateExternalBadges: function(values, labels) {
792
- if (!this.settings.showBadgesExternal) {
793
- return;
794
- }
795
-
796
- var $externalContainer = $('#' + this.settings.showBadgesExternal);
797
-
798
- if ($externalContainer.length === 0) {
799
- console.warn('OneSelect: External container not found - #' + this.settings.showBadgesExternal);
800
- return;
801
- }
802
-
803
- $externalContainer.empty();
804
-
805
- if (values.length === 0) {
806
- return;
807
- }
808
-
809
- var self = this;
810
- $.each(values, function(index, value) {
811
- var badge = $('<span class="cms-badge"></span>');
812
- var labelSpan = $('<span></span>').text(labels[index]);
813
- var removeBtn = $('<button type="button" class="cms-badge-remove">&times;</button>');
814
-
815
- removeBtn.on('click', function(e) {
816
- e.preventDefault();
817
- e.stopPropagation();
818
- self.unselect(value);
819
- });
820
-
821
- badge.append(labelSpan);
822
- badge.append(removeBtn);
823
- $externalContainer.append(badge);
824
- });
825
- },
826
-
827
- toggle: function() {
828
- if (this.wrapper.hasClass('open')) {
829
- this.close();
830
- } else {
831
- this.open();
832
- }
833
- },
834
-
835
- open: function() {
836
- // Close other open dropdowns
837
- $('.cms-wrapper.open').not(this.wrapper).removeClass('open');
838
- $('.cms-dropdown.open').not(this.dropdown).removeClass('open');
839
-
840
- // Calculate position
841
- this.updateDropdownPosition();
842
-
843
- this.wrapper.addClass('open');
844
- this.dropdown.addClass('open');
845
- },
846
-
847
- updateDropdownPosition: function() {
848
- var rect = this.wrapper[0].getBoundingClientRect();
849
- var wrapperHeight = this.wrapper.outerHeight();
850
- var wrapperWidth = this.wrapper.outerWidth();
851
-
852
- this.dropdown.css({
853
- position: 'fixed',
854
- top: rect.bottom + 'px',
855
- left: rect.left + 'px',
856
- width: wrapperWidth + 'px'
857
- });
858
- },
859
-
860
- close: function() {
861
- this.wrapper.removeClass('open');
862
- this.dropdown.removeClass('open');
863
- },
864
-
865
- handleOk: function() {
866
- this.updateTriggerText();
867
-
868
- var values = this.getSelectedValues();
869
- var labels = this.getSelectedLabels();
870
-
871
- if (this.settings.onOk) {
872
- this.settings.onOk.call(this, values, labels);
873
- }
874
-
875
- if (this.settings.submitForm) {
876
- this.submitForm();
877
- }
878
-
879
- this.close();
880
- },
881
-
882
- submitForm: function() {
883
- var form = null;
884
-
885
- if (this.settings.formId) {
886
- form = $('#' + this.settings.formId);
887
- if (form.length === 0) {
888
- console.warn('OneSelect: Form with ID "' + this.settings.formId + '" not found');
889
- return;
890
- }
891
- } else {
892
- form = this.$element.closest('form');
893
- if (form.length === 0) {
894
- console.warn('OneSelect: No parent form found');
895
- return;
896
- }
897
- }
898
-
899
- form[0].submit();
900
- },
901
-
902
- handleCancel: function() {
903
- this.settings.value = [];
904
- this.optionsContainer.find('input[type="checkbox"]').prop('checked', false);
905
- this.optionsContainer.find('.cms-option').removeClass('selected');
906
- this.updateSelectAllState();
907
- this.updateTriggerText();
908
- this.updateHiddenInputs();
909
-
910
- if (this.settings.onCancel) {
911
- this.settings.onCancel.call(this);
912
- }
913
-
914
- if (this.settings.submitForm) {
915
- this.submitForm();
916
- }
917
-
918
- this.close();
919
- },
920
-
921
- setValue: function(values) {
922
- this.settings.value = values || [];
923
- this.renderOptions();
924
- this.updateTriggerText();
925
- this.updateHiddenInputs();
926
- },
927
-
928
- getValue: function() {
929
- return this.getSelectedValues();
930
- },
931
-
932
- updateData: function(data) {
933
- this.settings.data = data || [];
934
- this.settings.value = [];
935
- this.renderOptions();
936
- this.updateTriggerText();
937
- this.updateHiddenInputs();
938
- },
939
-
940
- loadData: function(customAjaxConfig, onSuccess, onError) {
941
- var self = this;
942
- var ajaxConfig = customAjaxConfig || this.settings.ajax;
943
-
944
- if (!ajaxConfig || !ajaxConfig.url) {
945
- console.error('OneSelect: Ajax configuration or url is missing');
946
- return;
947
- }
948
-
949
- if (this.settings.beforeLoad) {
950
- this.settings.beforeLoad.call(this);
951
- }
952
-
953
- this.trigger.find('.cms-selected-text').text('Loading...');
954
-
955
- var request = $.extend(true, {
956
- url: ajaxConfig.url,
957
- method: ajaxConfig.method || 'GET',
958
- data: ajaxConfig.data || {},
959
- dataType: ajaxConfig.dataType || 'json',
960
- success: function(response) {
961
- var data = response;
962
- if (typeof response === 'object' && response.data) {
963
- data = response.data;
964
- } else if (typeof response === 'object' && response.results) {
965
- data = response.results;
966
- }
967
-
968
- self.settings.data = data || [];
969
- self.renderOptions();
970
- self.updateTriggerText();
971
-
972
- if (self.settings.afterLoad) {
973
- self.settings.afterLoad.call(self, data, response);
974
- }
975
-
976
- if (onSuccess) {
977
- onSuccess.call(self, data, response);
978
- }
979
- },
980
- error: function(xhr, status, error) {
981
- self.trigger.find('.cms-selected-text').text('Error loading data');
982
-
983
- if (self.settings.onLoadError) {
984
- self.settings.onLoadError.call(self, xhr, status, error);
985
- }
986
-
987
- if (onError) {
988
- onError.call(self, xhr, status, error);
989
- }
990
- }
991
- }, ajaxConfig);
992
-
993
- if (ajaxConfig.success) {
994
- var originalSuccess = request.success;
995
- request.success = function(response) {
996
- ajaxConfig.success(response);
997
- originalSuccess(response);
998
- };
999
- }
1000
-
1001
- if (ajaxConfig.error) {
1002
- var originalError = request.error;
1003
- request.error = function(xhr, status, error) {
1004
- ajaxConfig.error(xhr, status, error);
1005
- originalError(xhr, status, error);
1006
- };
1007
- }
1008
-
1009
- $.ajax(request);
1010
- },
1011
-
1012
- reload: function() {
1013
- if (this.settings.ajax) {
1014
- this.loadData();
1015
- } else {
1016
- console.warn('OneSelect: No ajax configuration found');
1017
- }
1018
- },
1019
-
1020
- select: function(value) {
1021
- var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + value + '"]');
1022
- if (checkbox.length) {
1023
- checkbox.prop('checked', true);
1024
- checkbox.closest('.cms-option').addClass('selected');
1025
- this.updateSelectAllState();
1026
- this.updateTriggerText();
1027
- this.updateHiddenInputs();
1028
- }
1029
- },
1030
-
1031
- unselect: function(value) {
1032
- var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + value + '"]');
1033
- if (checkbox.length) {
1034
- checkbox.prop('checked', false);
1035
- checkbox.closest('.cms-option').removeClass('selected');
1036
- this.updateSelectAllState();
1037
- this.updateTriggerText();
1038
- this.updateHiddenInputs();
1039
- }
1040
- },
1041
-
1042
- selectAll: function() {
1043
- this.handleSelectAll(true);
1044
- },
1045
-
1046
- unselectAll: function() {
1047
- this.handleSelectAll(false);
1048
- },
1049
-
1050
- toggleSelection: function(value) {
1051
- var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + value + '"]');
1052
- if (checkbox.length) {
1053
- var isChecked = checkbox.prop('checked');
1054
- checkbox.prop('checked', !isChecked);
1055
-
1056
- var option = checkbox.closest('.cms-option');
1057
- if (!isChecked) {
1058
- option.addClass('selected');
1059
- } else {
1060
- option.removeClass('selected');
1061
- }
1062
-
1063
- this.updateSelectAllState();
1064
- this.updateTriggerText();
1065
- this.updateHiddenInputs();
1066
- }
1067
- },
1068
-
1069
- getInstanceId: function() {
1070
- return this.instanceId;
1071
- },
1072
-
1073
- destroy: function() {
1074
- delete instances[this.instanceId];
1075
-
1076
- $(window).off('.cms');
1077
- $(window).off('.ones');
1078
- $(document).off('.onescroll');
1079
- this.trigger.off();
1080
- this.okBtn.off();
1081
- this.cancelBtn.off();
1082
- this.optionsContainer.off();
1083
-
1084
- // Clear horizontal scroll tracking interval
1085
- if (this._horizontalScrollInterval) {
1086
- clearInterval(this._horizontalScrollInterval);
1087
- this._horizontalScrollInterval = null;
1088
- }
1089
-
1090
- $('input.cms-hidden-input[data-cms-input="' + this.settings.name + '"]').remove();
1091
-
1092
- this.wrapper.remove();
1093
- this.dropdown.remove();
1094
- this.$element.removeData(pluginName);
1095
- }
1096
- };
1097
-
1098
- /**
1099
- * Get instance by ID
1100
- */
1101
- OneSelect.getInstance = function(instanceId) {
1102
- return instances[instanceId] || null;
1103
- };
1104
-
1105
- /**
1106
- * Get all instances
1107
- */
1108
- OneSelect.getAllInstances = function() {
1109
- return instances;
1110
- };
1111
-
1112
- /**
1113
- * jQuery Plugin Registration
1114
- */
1115
- $.fn[pluginName] = function(options) {
1116
- var args = arguments;
1117
- var returnValue = this;
1118
-
1119
- this.each(function() {
1120
- var $this = $(this);
1121
- var instance = $this.data(pluginName);
1122
-
1123
- // Initialize if not already
1124
- if (!instance) {
1125
- // Don't auto-initialize - only init if options are provided
1126
- if (typeof options === 'object' || !options) {
1127
- instance = new OneSelect(this, options);
1128
- $this.data(pluginName, instance);
1129
- } else {
1130
- // Method called without initialization
1131
- return;
1132
- }
1133
- }
1134
-
1135
- // Call method
1136
- if (typeof options === 'string') {
1137
- if (options === 'value') {
1138
- if (args[1] !== undefined) {
1139
- instance.setValue(args[1]);
1140
- returnValue = $this;
1141
- } else {
1142
- returnValue = instance.getValue();
1143
- }
1144
- } else if (options === 'getValues') {
1145
- returnValue = instance.getSelectedValues();
1146
- } else if (options === 'getLabels') {
1147
- returnValue = instance.getSelectedLabels();
1148
- } else if (options === 'getInstanceId') {
1149
- returnValue = instance.getInstanceId();
1150
- } else if (options === 'updateData') {
1151
- instance.updateData(args[1]);
1152
- } else if (options === 'loadData') {
1153
- instance.loadData(args[1], args[2], args[3]);
1154
- } else if (options === 'reload') {
1155
- instance.reload();
1156
- } else if (options === 'select') {
1157
- instance.select(args[1]);
1158
- } else if (options === 'unselect') {
1159
- instance.unselect(args[1]);
1160
- } else if (options === 'selectAll') {
1161
- instance.selectAll();
1162
- } else if (options === 'unselectAll') {
1163
- instance.unselectAll();
1164
- } else if (options === 'toggleSelection') {
1165
- instance.toggleSelection(args[1]);
1166
- } else if (options === 'open') {
1167
- instance.open();
1168
- } else if (options === 'close') {
1169
- instance.close();
1170
- } else if (options === 'destroy') {
1171
- instance.destroy();
1172
- }
1173
- }
1174
- });
1175
-
1176
- return returnValue;
1177
- };
1178
-
1179
- // Expose constructor
1180
- $.fn[pluginName].Constructor = OneSelect;
1181
-
1182
- // Expose static methods
1183
- $.fn[pluginName].getInstance = OneSelect.getInstance;
1184
- $.fn[pluginName].getAllInstances = OneSelect.getAllInstances;
1185
-
1186
- }));
1
+ /**
2
+ * OneSelect - jQuery Multi-Select Dropdown Plugin
3
+ * Version: 1.2.0
4
+ * https://github.com/your-repo/one-select
5
+ *
6
+ * Copyright 2026
7
+ * Licensed under MIT
8
+ *
9
+ * A powerful, flexible, and feature-rich multi-select dropdown component for jQuery.
10
+ */
11
+
12
+ (function (factory) {
13
+ 'use strict';
14
+
15
+ if (typeof define === 'function' && define.amd) {
16
+ // AMD
17
+ define(['jquery'], factory);
18
+ } else if (typeof module === 'object' && module.exports) {
19
+ // CommonJS
20
+ module.exports = function (root, jQuery) {
21
+ if (jQuery === undefined) {
22
+ if (typeof window !== 'undefined') {
23
+ jQuery = require('jquery');
24
+ } else {
25
+ jQuery = require('jquery')(root);
26
+ }
27
+ }
28
+ factory(jQuery);
29
+ return jQuery;
30
+ };
31
+ } else {
32
+ // Browser globals
33
+ factory(window.jQuery || window.$);
34
+ }
35
+
36
+ }(function ($) {
37
+ 'use strict';
38
+
39
+ // Global registry for all instances
40
+ var instances = {};
41
+ var pluginName = 'oneSelect';
42
+
43
+ /**
44
+ * Debounce utility function
45
+ * @param {Function} func - Function to debounce
46
+ * @param {Number} delay - Delay in milliseconds
47
+ * @returns {Function} Debounced function
48
+ */
49
+ function debounce(func, delay) {
50
+ var timeoutId;
51
+ return function () {
52
+ var context = this;
53
+ var args = arguments;
54
+ clearTimeout(timeoutId);
55
+ timeoutId = setTimeout(function () {
56
+ func.apply(context, args);
57
+ }, delay);
58
+ };
59
+ }
60
+
61
+ /**
62
+ * OneSelect Constructor
63
+ * @param {HTMLElement} element - The DOM element
64
+ * @param {Object} options - Configuration options
65
+ */
66
+ var OneSelect = function (element, options) {
67
+ this.element = element;
68
+ this.$element = $(element);
69
+
70
+ // Generate unique instance ID for control
71
+ this.instanceId = 'ones-' + Math.random().toString(36).substr(2, 9);
72
+
73
+ // Read data attributes from element (highest priority)
74
+ var dataOptions = this.readDataAttributes();
75
+
76
+ // Merge: defaults -> options (JS) -> dataOptions (HTML attributes)
77
+ this.settings = $.extend({}, OneSelect.defaults, options, dataOptions);
78
+ this.init();
79
+ };
80
+
81
+ /**
82
+ * Default configuration options
83
+ */
84
+ OneSelect.defaults = {
85
+ placeholder: 'Select options...',
86
+ selectAllText: 'Select All',
87
+ okText: 'OK',
88
+ cancelText: 'Cancel',
89
+ data: [],
90
+ value: null, // Single value or array of values to pre-select (index-based)
91
+ showCheckbox: true,
92
+ showBadges: false,
93
+ showBadgesExternal: null,
94
+ showSearch: false, // Show search input in dropdown
95
+ searchPlaceholder: 'Search...',
96
+ searchUrl: null, // URL for AJAX search (GET request)
97
+ searchDebounceDelay: 300,// Delay in milliseconds for search debounce
98
+ closeOnScroll: false,
99
+ closeOnOutside: true, // Close dropdown when clicking outside (default: true)
100
+ submitForm: false,
101
+ submitOnOutside: false,
102
+ formId: null,
103
+ name: null,
104
+ multiple: true,
105
+ ajax: null,
106
+ autoLoad: true,
107
+ beforeLoad: null,
108
+ afterLoad: null,
109
+ onLoadError: null,
110
+ onChange: null,
111
+ onSelect: null,
112
+ onOk: null,
113
+ onCancel: null,
114
+ infinityScroll: false,
115
+ onInfinityScroll: null,
116
+ loading: false
117
+ };
118
+
119
+ /**
120
+ * OneSelect Prototype
121
+ */
122
+ OneSelect.prototype = {
123
+ /**
124
+ * Read data attributes from HTML element
125
+ */
126
+ readDataAttributes: function () {
127
+ var self = this;
128
+ var dataOptions = {};
129
+
130
+ var attributeMap = {
131
+ 'ones-placeholder': 'placeholder',
132
+ 'ones-select-all-text': 'selectAllText',
133
+ 'ones-ok-text': 'okText',
134
+ 'ones-cancel-text': 'cancelText',
135
+ 'ones-data': 'data',
136
+ 'ones-value': 'value',
137
+ 'ones-name': 'name',
138
+ 'ones-multiple': 'multiple',
139
+ 'ones-show-checkbox': 'showCheckbox',
140
+ 'ones-show-badges': 'showBadges',
141
+ 'ones-show-badges-external': 'showBadgesExternal',
142
+ 'ones-show-search': 'showSearch',
143
+ 'ones-search-placeholder': 'searchPlaceholder',
144
+ 'ones-search-url': 'searchUrl',
145
+ 'ones-search-debounce-delay': 'searchDebounceDelay',
146
+ 'ones-close-on-scroll': 'closeOnScroll',
147
+ 'ones-close-on-outside': 'closeOnOutside',
148
+ 'ones-submit-form': 'submitForm',
149
+ 'ones-submit-on-outside': 'submitOnOutside',
150
+ 'ones-form-id': 'formId',
151
+ 'ones-auto-load': 'autoLoad',
152
+ 'ones-infinity-scroll': 'infinityScroll'
153
+ };
154
+
155
+ $.each(attributeMap, function (attr, setting) {
156
+ var value = self.$element.data(attr);
157
+
158
+ if (value === undefined) {
159
+ return;
160
+ }
161
+
162
+ if (setting === 'data' || setting === 'value') {
163
+ if (typeof value === 'string') {
164
+ try {
165
+ dataOptions[setting] = JSON.parse(value);
166
+ } catch (e) {
167
+ console.warn('OneSelect: Invalid JSON for ' + attr, value);
168
+ dataOptions[setting] = value;
169
+ }
170
+ } else {
171
+ dataOptions[setting] = value;
172
+ }
173
+ } else if (setting === 'multiple' || setting === 'showCheckbox' ||
174
+ setting === 'showBadges' || setting === 'showSearch' ||
175
+ setting === 'closeOnScroll' || setting === 'closeOnOutside' ||
176
+ setting === 'submitForm' || setting === 'submitOnOutside' ||
177
+ setting === 'autoLoad' || setting === 'infinityScroll') {
178
+ if (typeof value === 'string') {
179
+ dataOptions[setting] = value === 'true' || value === '1';
180
+ } else {
181
+ dataOptions[setting] = !!value;
182
+ }
183
+ } else if (setting === 'searchDebounceDelay') {
184
+ // Parse as number
185
+ if (typeof value === 'string') {
186
+ dataOptions[setting] = parseInt(value, 10) || 300;
187
+ } else {
188
+ dataOptions[setting] = value || 300;
189
+ }
190
+ } else {
191
+ dataOptions[setting] = value;
192
+ }
193
+ });
194
+
195
+ var ajaxData = this.$element.data('ones-ajax');
196
+ if (ajaxData) {
197
+ if (typeof ajaxData === 'string') {
198
+ // String URL olarak kullan, default GET method ile
199
+ dataOptions.ajax = {
200
+ url: ajaxData,
201
+ method: 'GET'
202
+ };
203
+ } else if (typeof ajaxData === 'object') {
204
+ // Object olarak kullan (detaylı konfigürasyon için)
205
+ dataOptions.ajax = ajaxData;
206
+ }
207
+ }
208
+
209
+ return dataOptions;
210
+ },
211
+
212
+ init: function () {
213
+ // Register instance in global registry
214
+ instances[this.instanceId] = this;
215
+
216
+ // Convert value to array if needed and sanitize
217
+ if (this.settings.value !== null && this.settings.value !== undefined) {
218
+ if (!Array.isArray(this.settings.value)) {
219
+ this.settings.value = [this.settings.value];
220
+ }
221
+
222
+ // Remove null/undefined and duplicates
223
+ var seen = {};
224
+ this.settings.value = this.settings.value.filter(function (v) {
225
+ if (v === null || v === undefined) return false;
226
+ var strV = String(v).trim();
227
+ if (seen[strV]) return false;
228
+ seen[strV] = true;
229
+ return true;
230
+ });
231
+ } else {
232
+ this.settings.value = [];
233
+ }
234
+
235
+ // Sync initial value to data attribute
236
+ this.updateDataValueAttribute();
237
+
238
+ // Initialize pagination state for infinity scroll
239
+ this.currentPage = 1;
240
+ this.hasNextPage = false;
241
+
242
+ this.wrapper = this.createWrapper();
243
+ this.trigger = this.createTrigger();
244
+ this.dropdown = this.createDropdown();
245
+ this.searchInput = this.createSearchInput();
246
+ this.optionsContainer = this.createOptionsContainer();
247
+ this.preloader = this.createPreloader();
248
+ this.buttons = this.createButtons();
249
+
250
+ this.build();
251
+ this.attachEvents();
252
+
253
+ if (this.settings.ajax && this.settings.autoLoad) {
254
+ this.loadData();
255
+ }
256
+ },
257
+
258
+ build: function () {
259
+ // Add search input at the top of dropdown if enabled
260
+ if (this.settings.showSearch) {
261
+ this.dropdown.append(this.searchInput);
262
+ }
263
+ this.dropdown.append(this.optionsContainer);
264
+ this.dropdown.append(this.preloader);
265
+ this.dropdown.append(this.buttons);
266
+ this.wrapper.append(this.trigger);
267
+
268
+ // Append wrapper to $element, dropdown to body
269
+ this.$element.append(this.wrapper);
270
+ $('body').append(this.dropdown);
271
+
272
+ this.renderOptions();
273
+ this.updateTriggerText();
274
+ this.updateHiddenInputs();
275
+ },
276
+
277
+ updateHiddenInputs: function () {
278
+ if (!this.settings.name) {
279
+ return;
280
+ }
281
+
282
+ var form = null;
283
+ if (this.settings.formId) {
284
+ form = $('#' + this.settings.formId);
285
+ } else {
286
+ form = this.$element.closest('form');
287
+ }
288
+
289
+ var container = form.length ? form : this.wrapper;
290
+
291
+ container.find('input.cms-hidden-input[data-cms-input="' + this.settings.name + '"]').remove();
292
+
293
+ var inputName = this.settings.name;
294
+ if (this.settings.multiple && inputName.indexOf('[') === -1) {
295
+ inputName += '[]';
296
+ }
297
+
298
+ var selectedValues = this.getSelectedValues();
299
+
300
+ // Filter out null/undefined/empty to prevent backend errors
301
+ if (Array.isArray(selectedValues)) {
302
+ selectedValues = selectedValues.filter(function (v) {
303
+ return v !== null && v !== undefined && String(v).trim() !== '';
304
+ });
305
+ }
306
+
307
+ if (this.settings.multiple) {
308
+ if (selectedValues.length === 0) {
309
+ var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
310
+ .attr('name', this.settings.name)
311
+ .attr('value', '')
312
+ .attr('data-cms-input', this.settings.name);
313
+ container.append(hiddenInput);
314
+ } else {
315
+ $.each(selectedValues, function (index, value) {
316
+ var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
317
+ .attr('name', inputName)
318
+ .attr('value', value)
319
+ .attr('data-cms-input', this.settings.name)
320
+ .attr('data-cms-value', value);
321
+ container.append(hiddenInput);
322
+ }.bind(this));
323
+ }
324
+ } else {
325
+ var value = selectedValues.length > 0 ? selectedValues[0] : '';
326
+ var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
327
+ .attr('name', inputName)
328
+ .attr('value', value)
329
+ .attr('data-cms-input', this.settings.name);
330
+ container.append(hiddenInput);
331
+ }
332
+ },
333
+
334
+ createWrapper: function () {
335
+ return $('<div class="cms-wrapper"></div>');
336
+ },
337
+
338
+ createTrigger: function () {
339
+ return $('<div class="cms-trigger"><span class="cms-selected-text cms-placeholder">' +
340
+ this.settings.placeholder + '</span></div>');
341
+ },
342
+
343
+ createDropdown: function () {
344
+ return $('<div class="cms-dropdown"></div>');
345
+ },
346
+
347
+ createSearchInput: function () {
348
+ return $('<div class="cms-search-wrapper">' +
349
+ '<input type="text" class="cms-search-input" placeholder="' +
350
+ this.settings.searchPlaceholder + '" /></div>');
351
+ },
352
+
353
+ createOptionsContainer: function () {
354
+ return $('<div class="cms-options-container"></div>');
355
+ },
356
+
357
+ createPreloader: function () {
358
+ return $('<div class="cms-infinity-preloader"><div class="cms-spinner"></div></div>');
359
+ },
360
+
361
+ createButtons: function () {
362
+ var container = $('<div class="cms-buttons"></div>');
363
+ this.okBtn = $('<button class="cms-btn cms-btn-ok">' + this.settings.okText + '</button>');
364
+ this.cancelBtn = $('<button class="cms-btn cms-btn-cancel">' + this.settings.cancelText + '</button>');
365
+
366
+ container.append(this.okBtn);
367
+ container.append(this.cancelBtn);
368
+
369
+ return container;
370
+ },
371
+
372
+ renderOptions: function () {
373
+ this.optionsContainer.empty();
374
+
375
+ var selectAllOption = this.createOption('select-all', this.settings.selectAllText, false);
376
+ this.optionsContainer.append(selectAllOption);
377
+
378
+ var self = this;
379
+ $.each(this.settings.data, function (key, label) {
380
+ // For object: key = form value, label = display text
381
+ // For array: key = index, label = item
382
+ var value = key;
383
+ var label = label;
384
+
385
+ var isSelected = self.isValueSelected(value);
386
+ var option = self.createOption(value, label, isSelected);
387
+ self.optionsContainer.append(option);
388
+ });
389
+
390
+ this.updateSelectAllState();
391
+ },
392
+ htmlEncode: function (str) {
393
+ return String(str)
394
+ .replace(/&/g, "&amp;")
395
+ .replace(/</g, "&lt;")
396
+ .replace(/>/g, "&gt;")
397
+ .replace(/"/g, "&quot;")
398
+ .replace(/'/g, "&#39;");
399
+ },
400
+
401
+ isValueSelected: function (value) {
402
+ if (!this.settings.value) return false;
403
+ if (Array.isArray(this.settings.value)) {
404
+ var strValue = String(value).trim();
405
+ return this.settings.value.some(function (v) {
406
+ return String(v).trim() === strValue;
407
+ });
408
+ }
409
+ return String(this.settings.value).trim() === String(value).trim();
410
+ },
411
+
412
+ /**
413
+ * Update the data-ones-value attribute to keep it in sync with settings.value
414
+ * Removes attribute if value is empty, otherwise sets it to JSON array
415
+ */
416
+ updateDataValueAttribute: function () {
417
+ if (!this.settings.value || this.settings.value.length === 0) {
418
+ // Remove attribute completely when empty
419
+ this.$element.removeAttr('data-ones-value');
420
+ } else {
421
+ // Update with current value as JSON
422
+ this.$element.attr('data-ones-value', JSON.stringify(this.settings.value));
423
+ }
424
+ },
425
+
426
+ /**
427
+ * Append new options to existing list (for pagination)
428
+ * @param {Object} data - New data to append
429
+ */
430
+ appendOptions: function (data) {
431
+ var self = this;
432
+ $.each(data, function (key, label) {
433
+ var value = key;
434
+ var label = label;
435
+
436
+ // Check if option already exists
437
+ var existingOption = self.optionsContainer.find('.cms-option[data-value="' + self.htmlEncode(value) + '"]');
438
+ if (existingOption.length > 0) {
439
+ return; // Skip if already exists
440
+ }
441
+
442
+ var isSelected = self.isValueSelected(value);
443
+ var option = self.createOption(value, label, isSelected);
444
+ self.optionsContainer.append(option);
445
+ });
446
+
447
+ this.updateSelectAllState();
448
+ },
449
+
450
+ createOption: function (value, label, checked) {
451
+ var optionClass = 'cms-option';
452
+ if (!this.settings.showCheckbox) {
453
+ optionClass += ' cms-hide-checkbox';
454
+ }
455
+
456
+ if (checked && value !== 'select-all') {
457
+ optionClass += ' selected';
458
+ }
459
+
460
+ var option = $('<div class="' + optionClass + '" data-value="' + this.htmlEncode(value) + '"></div>');
461
+ var checkbox = $('<input type="checkbox" value="' + this.htmlEncode(value) + '"' +
462
+ (checked ? ' checked' : '') + '>');
463
+ var labelEl = $('<label>' + label + '</label>');
464
+
465
+ option.append(checkbox);
466
+ option.append(labelEl);
467
+
468
+ return option;
469
+ },
470
+
471
+ /**
472
+ * Filter options based on search text
473
+ * @param {String} searchText - Search text to filter by
474
+ */
475
+ filterOptions: function (searchText) {
476
+ var self = this;
477
+ var options = this.optionsContainer.find('.cms-option:not([data-value="select-all"])');
478
+
479
+ if (searchText === '') {
480
+ // Show all options if search is empty
481
+ options.show();
482
+ } else {
483
+ // Filter options by label
484
+ options.each(function () {
485
+ var option = $(this);
486
+ var label = option.find('label').text().toLowerCase();
487
+
488
+ if (label.indexOf(searchText) !== -1) {
489
+ option.show();
490
+ } else {
491
+ option.hide();
492
+ }
493
+ });
494
+ }
495
+ },
496
+
497
+ /**
498
+ * Perform AJAX search
499
+ * @param {String} searchText - Search text to send to server
500
+ */
501
+ performAjaxSearch: function (searchText) {
502
+ var self = this;
503
+
504
+ // Show loading state
505
+ this.optionsContainer.addClass('cms-loading');
506
+
507
+ $.ajax({
508
+ url: this.settings.searchUrl,
509
+ method: 'GET',
510
+ data: { q: searchText },
511
+ dataType: 'json',
512
+ success: function (response) {
513
+ // Handle different response formats
514
+ var data = response;
515
+ if (typeof response === 'object' && response.data) {
516
+ data = response.data;
517
+ } else if (typeof response === 'object' && response.results) {
518
+ data = response.results;
519
+ }
520
+
521
+ // Update dropdown with search results
522
+ self.updateSearchResults(data || []);
523
+
524
+ self.optionsContainer.removeClass('cms-loading');
525
+ },
526
+ error: function (xhr, status, error) {
527
+ console.error('OneSelect: Search error', error);
528
+ self.optionsContainer.removeClass('cms-loading');
529
+
530
+ if (self.settings.onLoadError) {
531
+ self.settings.onLoadError.call(self, xhr, status, error);
532
+ }
533
+ }
534
+ });
535
+ },
536
+
537
+ /**
538
+ * Update dropdown with search results
539
+ * @param {Array} data - Search results data
540
+ */
541
+ updateSearchResults: function (data) {
542
+ // Clear existing options (except select-all)
543
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"])').remove();
544
+
545
+ var self = this;
546
+ $.each(data, function (key, label) {
547
+ // For object: key = form value, label = display text
548
+ // For array: key = index, label = item
549
+ var value = key;
550
+ var label = label;
551
+
552
+ var isSelected = self.isValueSelected(value);
553
+ var option = self.createOption(value, label, isSelected);
554
+ self.optionsContainer.append(option);
555
+ });
556
+
557
+ // Update select-all state
558
+ this.updateSelectAllState();
559
+ },
560
+
561
+ attachEvents: function () {
562
+ var self = this;
563
+
564
+ this.trigger.on('click', function (e) {
565
+ e.stopPropagation();
566
+ self.toggle();
567
+ });
568
+
569
+ // Search input event listener
570
+ if (this.settings.showSearch) {
571
+ if (this.settings.searchUrl) {
572
+ // AJAX search with debounce
573
+ var debouncedSearch = debounce(function (searchText) {
574
+ self.performAjaxSearch(searchText);
575
+ }, this.settings.searchDebounceDelay);
576
+
577
+ this.searchInput.find('.cms-search-input').on('keyup', function () {
578
+ var searchText = $(this).val();
579
+ if (searchText.length > 0) {
580
+ debouncedSearch(searchText);
581
+ } else {
582
+ // Show original data when search is empty
583
+ self.filterOptions('');
584
+ }
585
+ });
586
+ } else {
587
+ // Local filtering (default)
588
+ this.searchInput.find('.cms-search-input').on('keyup', function () {
589
+ var searchText = $(this).val().toLowerCase();
590
+ self.filterOptions(searchText);
591
+ });
592
+ }
593
+ }
594
+
595
+ $(window).on('resize.cms', function () {
596
+ if (self.wrapper.hasClass('open')) {
597
+ self.updateDropdownPosition();
598
+ }
599
+ });
600
+
601
+ if (this.settings.closeOnScroll) {
602
+ $(window).on('scroll.cms', function () {
603
+ if (self.wrapper.hasClass('open')) {
604
+ self.close();
605
+ }
606
+ });
607
+ } else {
608
+ // Update dropdown position on vertical scroll
609
+ $(window).on('scroll.cms', function () {
610
+ if (self.wrapper.hasClass('open')) {
611
+ self.updateDropdownPosition();
612
+ }
613
+ });
614
+ }
615
+
616
+ // Global horizontal scroll handler - close dropdown on any horizontal scroll
617
+ // Listen for wheel events with horizontal delta
618
+ $(document).on('wheel.onescroll', function (e) {
619
+ if (!self.wrapper.hasClass('open')) {
620
+ return;
621
+ }
622
+
623
+ // Check if horizontal scrolling (deltaX != 0)
624
+ if (e.originalEvent && Math.abs(e.originalEvent.deltaX) > 0) {
625
+ self.close();
626
+ }
627
+ });
628
+
629
+ // Also listen for scroll events on all elements to detect horizontal scroll
630
+ // Using MutationObserver to detect when elements with overflow scroll
631
+ self._detectHorizontalScroll = function () {
632
+ if (!self.wrapper.hasClass('open')) return;
633
+
634
+ // Check window horizontal scroll
635
+ if (window.scrollX !== self._lastWindowScrollX) {
636
+ self._lastWindowScrollX = window.scrollX;
637
+ if (self._lastWindowScrollX > 0) {
638
+ self.close();
639
+ return;
640
+ }
641
+ }
642
+
643
+ // Check all scrollable elements for horizontal scroll
644
+ var scrollableElements = document.querySelectorAll('*');
645
+ for (var i = 0; i < scrollableElements.length; i++) {
646
+ var el = scrollableElements[i];
647
+ var key = getElementKey(el);
648
+
649
+ if (self._elementScrollPositions[key] !== undefined) {
650
+ var currentScroll = el.scrollLeft;
651
+ if (currentScroll !== self._elementScrollPositions[key]) {
652
+ // Horizontal scroll detected
653
+ self.close();
654
+ return;
655
+ }
656
+ }
657
+ }
658
+ };
659
+
660
+ // Store scroll positions and track changes
661
+ self._elementScrollPositions = {};
662
+ self._lastWindowScrollX = window.scrollX;
663
+
664
+ function getElementKey(el) {
665
+ if (el === document) return 'document';
666
+ if (el === document.documentElement) return 'html';
667
+ if (el === document.body) return 'body';
668
+ return el.tagName + '-' + (el.id || el.className || Math.random().toString(36).substr(2, 9));
669
+ }
670
+
671
+ // Override open to initialize tracking
672
+ var originalOpen = self.open.bind(self);
673
+ self.open = function () {
674
+ // Store initial scroll positions
675
+ self._elementScrollPositions = {};
676
+ self._lastWindowScrollX = window.scrollX;
677
+
678
+ var scrollableElements = document.querySelectorAll('*');
679
+ for (var i = 0; i < scrollableElements.length; i++) {
680
+ var el = scrollableElements[i];
681
+ if (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) {
682
+ self._elementScrollPositions[getElementKey(el)] = el.scrollLeft;
683
+ }
684
+ }
685
+
686
+ // Start checking periodically
687
+ if (self._horizontalScrollInterval) {
688
+ clearInterval(self._horizontalScrollInterval);
689
+ }
690
+ self._horizontalScrollInterval = setInterval(function () {
691
+ self._detectHorizontalScroll();
692
+ }, 50);
693
+
694
+ originalOpen();
695
+ };
696
+
697
+ // Override close to stop tracking
698
+ var originalClose = self.close.bind(self);
699
+ self.close = function () {
700
+ if (self._horizontalScrollInterval) {
701
+ clearInterval(self._horizontalScrollInterval);
702
+ self._horizontalScrollInterval = null;
703
+ }
704
+ self._elementScrollPositions = {};
705
+ originalClose();
706
+ };
707
+
708
+ // Window click handler - close dropdown when clicking outside
709
+ $(window).on('click.ones', function (e) {
710
+ if (!self.settings.closeOnOutside) {
711
+ return;
712
+ }
713
+
714
+ if (!self.wrapper.hasClass('open')) {
715
+ return;
716
+ }
717
+
718
+ var $target = $(e.target);
719
+ // Don't close if clicked inside dropdown or wrapper elements
720
+ if ($target.closest('.cms-wrapper').length > 0 ||
721
+ $target.closest('.cms-dropdown').length > 0) {
722
+ return;
723
+ }
724
+
725
+ // Submit form if submitOnOutside is enabled
726
+ if (self.settings.submitOnOutside) {
727
+ self.updateTriggerText();
728
+ if (self.settings.onOk) {
729
+ self.settings.onOk.call(self, self.getSelectedValues(), self.getSelectedLabels());
730
+ }
731
+ self.submitForm();
732
+ }
733
+
734
+ self.close();
735
+ });
736
+
737
+ this.optionsContainer.on('click', '.cms-option', function (e) {
738
+ var $target = $(e.target);
739
+
740
+ // If clicking label or checkbox, browser already handles toggle
741
+ if ($target.is('input[type="checkbox"]') || $target.closest('label').length > 0 || $target.closest('button').length > 0) {
742
+ return;
743
+ }
744
+
745
+ e.preventDefault();
746
+ e.stopPropagation();
747
+
748
+ var option = $(this);
749
+ var checkbox = option.find('input[type="checkbox"]');
750
+ checkbox.prop('checked', !checkbox.prop('checked')).trigger('change');
751
+ });
752
+
753
+ this.optionsContainer.on('change', 'input[type="checkbox"]', function (e) {
754
+ e.stopPropagation();
755
+ var checkbox = $(this);
756
+ var option = checkbox.closest('.cms-option');
757
+ var value = option.data('value');
758
+
759
+ if (value === 'select-all') {
760
+ self.handleSelectAll(checkbox.prop('checked'));
761
+ } else {
762
+ self.handleOptionChange(option);
763
+ }
764
+ });
765
+
766
+ this.okBtn.on('click', function (e) {
767
+ e.stopPropagation();
768
+ self.handleOk();
769
+ });
770
+
771
+ this.cancelBtn.on('click', function (e) {
772
+ e.stopPropagation();
773
+ self.handleCancel();
774
+ });
775
+
776
+ // Infinity Scroll with Debounce - only activate if AJAX is configured
777
+ if (this.settings.infinityScroll && this.settings.ajax && this.settings.ajax.url) {
778
+ var debouncedScroll = debounce(function () {
779
+ // Check if we have more pages to load
780
+ if (!self.hasNextPage) {
781
+ return;
782
+ }
783
+
784
+ if (!self.settings.loading) { // Double check loading state
785
+ self.loadNextPage();
786
+ }
787
+ }, 200); // 200ms debounce delay
788
+
789
+ this.optionsContainer.on('scroll', function () {
790
+ var container = $(this);
791
+ if (container.scrollTop() + container.innerHeight() >= container[0].scrollHeight - 50) {
792
+ debouncedScroll();
793
+ }
794
+ });
795
+ }
796
+
797
+ // Legacy onInfinityScroll callback support (deprecated)
798
+ if (this.settings.infinityScroll && this.settings.onInfinityScroll && !this.settings.ajax) {
799
+ console.warn('OneSelect: infinityScroll requires ajax configuration. onInfinityScroll callback is deprecated.');
800
+ }
801
+ },
802
+
803
+ handleOptionChange: function (option) {
804
+ var value = option.data('value');
805
+ var checkbox = option.find('input[type="checkbox"]');
806
+ var isChecked = checkbox.prop('checked');
807
+
808
+ if (isChecked) {
809
+ this.select(value);
810
+ } else {
811
+ this.unselect(value);
812
+ }
813
+
814
+ if (this.settings.onChange) {
815
+ this.settings.onChange.call(this, this.getSelectedValues(), this.getSelectedLabels());
816
+ }
817
+
818
+ if (this.settings.onSelect) {
819
+ this.settings.onSelect.call(this, this.getSelectedValues());
820
+ }
821
+ },
822
+
823
+ handleSelectAll: function (checked) {
824
+ var self = this;
825
+ if (!checked) {
826
+ // Clear all selection (including hidden ones)
827
+ if (Array.isArray(this.settings.value)) {
828
+ this.settings.value = [];
829
+ }
830
+ }
831
+
832
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"])').each(function () {
833
+ var option = $(this);
834
+ option.find('input[type="checkbox"]').prop('checked', checked);
835
+ if (checked) {
836
+ option.addClass('selected');
837
+ // Add to settings.value if not exists
838
+ var val = option.find('input[type="checkbox"]').val();
839
+ if (Array.isArray(self.settings.value)) {
840
+ if (!self.isValueSelected(val)) {
841
+ self.settings.value.push(val);
842
+ }
843
+ }
844
+ } else {
845
+ option.removeClass('selected');
846
+ }
847
+ });
848
+
849
+ this.updateSelectAllState();
850
+ this.updateTriggerText();
851
+ this.updateHiddenInputs();
852
+ this.updateDataValueAttribute();
853
+ },
854
+
855
+ updateSelectAllState: function () {
856
+ var allOptions = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]');
857
+ var checkedOptions = allOptions.filter(':checked');
858
+ var totalCount = allOptions.length;
859
+ var checkedCount = checkedOptions.length;
860
+
861
+ var selectAllCheckbox = this.optionsContainer.find('.cms-option[data-value="select-all"] input[type="checkbox"]');
862
+
863
+ selectAllCheckbox.prop('indeterminate', false);
864
+ selectAllCheckbox.prop('checked', false);
865
+
866
+ if (checkedCount === 0) {
867
+ // Nothing selected
868
+ } else if (checkedCount === totalCount && totalCount > 0) {
869
+ selectAllCheckbox.prop('checked', true);
870
+ } else {
871
+ selectAllCheckbox.prop('indeterminate', true);
872
+ }
873
+ },
874
+
875
+ getSelectedValues: function () {
876
+ // Return unique sanitized settings.value
877
+ if (!Array.isArray(this.settings.value)) {
878
+ return [];
879
+ }
880
+ return this.settings.value;
881
+ },
882
+
883
+ getSelectedLabels: function () {
884
+ var labels = [];
885
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]:checked')
886
+ .siblings('label')
887
+ .each(function () {
888
+ labels.push($(this).text());
889
+ });
890
+ return labels;
891
+ },
892
+
893
+ updateTriggerText: function () {
894
+ var labels = this.getSelectedLabels();
895
+ var values = this.getSelectedValues();
896
+ var totalCount = this.settings.value ? this.settings.value.length : 0;
897
+
898
+ var textSpan = this.trigger.find('.cms-selected-text');
899
+
900
+ if (labels.length === 0) {
901
+ textSpan.empty().text(this.settings.placeholder).addClass('cms-placeholder');
902
+ } else if (this.settings.showBadges) {
903
+ textSpan.empty().removeClass('cms-placeholder');
904
+
905
+ var self = this;
906
+ var self = this;
907
+ $.each(values, function (index, value) {
908
+ var badge = $('<span class="cms-badge"></span>');
909
+
910
+ // Find label from DOM (if exists) or fallback to value
911
+ var labelText = value;
912
+ var option = self.optionsContainer.find('.cms-option[data-value="' + self.htmlEncode(value) + '"]');
913
+ if (option.length) {
914
+ labelText = option.find('label').text();
915
+ }
916
+
917
+ var labelSpan = $('<span></span>').text(labelText);
918
+ var removeBtn = $('<button type="button" class="cms-badge-remove">&times;</button>');
919
+
920
+ removeBtn.on('click', function (e) {
921
+ e.stopPropagation();
922
+ self.unselect(value);
923
+ });
924
+
925
+ badge.append(labelSpan);
926
+ badge.append(removeBtn);
927
+ textSpan.append(badge);
928
+ });
929
+ } else {
930
+ textSpan.empty().removeClass('cms-placeholder');
931
+ textSpan.text(totalCount + ' items selected');
932
+ }
933
+
934
+ this.updateExternalBadges(values, labels);
935
+ },
936
+
937
+ updateExternalBadges: function (values, labels) {
938
+ if (!this.settings.showBadgesExternal) {
939
+ return;
940
+ }
941
+
942
+ var $externalContainer = $('#' + this.settings.showBadgesExternal);
943
+
944
+ if ($externalContainer.length === 0) {
945
+ console.warn('OneSelect: External container not found - #' + this.settings.showBadgesExternal);
946
+ return;
947
+ }
948
+
949
+ $externalContainer.empty();
950
+
951
+ if (values.length === 0) {
952
+ return;
953
+ }
954
+
955
+ var self = this;
956
+ $.each(values, function (index, value) {
957
+ var badge = $('<span class="cms-badge"></span>');
958
+ var labelSpan = $('<span></span>').text(labels[index]);
959
+ var removeBtn = $('<button type="button" class="cms-badge-remove">&times;</button>');
960
+
961
+ removeBtn.on('click', function (e) {
962
+ e.preventDefault();
963
+ e.stopPropagation();
964
+ self.unselect(value);
965
+ });
966
+
967
+ badge.append(labelSpan);
968
+ badge.append(removeBtn);
969
+ $externalContainer.append(badge);
970
+ });
971
+ },
972
+
973
+ toggle: function () {
974
+ if (this.wrapper.hasClass('open')) {
975
+ this.close();
976
+ } else {
977
+ this.open();
978
+ }
979
+ },
980
+
981
+ open: function () {
982
+ // Close other open dropdowns
983
+ $('.cms-wrapper.open').not(this.wrapper).removeClass('open');
984
+ $('.cms-dropdown.open').not(this.dropdown).removeClass('open');
985
+
986
+ // Calculate position
987
+ this.updateDropdownPosition();
988
+
989
+ this.wrapper.addClass('open');
990
+ this.dropdown.addClass('open');
991
+ },
992
+
993
+ updateDropdownPosition: function () {
994
+ var rect = this.wrapper[0].getBoundingClientRect();
995
+ var wrapperHeight = this.wrapper.outerHeight();
996
+ var wrapperWidth = this.wrapper.outerWidth();
997
+
998
+ this.dropdown.css({
999
+ position: 'fixed',
1000
+ top: rect.bottom + 'px',
1001
+ left: rect.left + 'px',
1002
+ width: wrapperWidth + 'px'
1003
+ });
1004
+ },
1005
+
1006
+ close: function () {
1007
+ this.wrapper.removeClass('open');
1008
+ this.dropdown.removeClass('open');
1009
+ },
1010
+
1011
+ handleOk: function () {
1012
+ this.updateTriggerText();
1013
+
1014
+ var values = this.getSelectedValues();
1015
+ var labels = this.getSelectedLabels();
1016
+
1017
+ if (this.settings.onOk) {
1018
+ this.settings.onOk.call(this, values, labels);
1019
+ }
1020
+
1021
+ if (this.settings.submitForm) {
1022
+ this.submitForm();
1023
+ }
1024
+
1025
+ this.updateDataValueAttribute();
1026
+ this.close();
1027
+ },
1028
+
1029
+ submitForm: function () {
1030
+ var form = null;
1031
+
1032
+ if (this.settings.formId) {
1033
+ form = $('#' + this.settings.formId);
1034
+ if (form.length === 0) {
1035
+ console.warn('OneSelect: Form with ID "' + this.settings.formId + '" not found');
1036
+ return;
1037
+ }
1038
+ } else {
1039
+ form = this.$element.closest('form');
1040
+ if (form.length === 0) {
1041
+ console.warn('OneSelect: No parent form found');
1042
+ return;
1043
+ }
1044
+ }
1045
+
1046
+ form[0].submit();
1047
+ },
1048
+
1049
+ handleCancel: function () {
1050
+ this.settings.value = [];
1051
+ this.optionsContainer.find('input[type="checkbox"]').prop('checked', false);
1052
+ this.optionsContainer.find('.cms-option').removeClass('selected');
1053
+ this.updateSelectAllState();
1054
+ this.updateTriggerText();
1055
+ this.updateHiddenInputs();
1056
+ this.updateDataValueAttribute();
1057
+
1058
+ if (this.settings.onCancel) {
1059
+ this.settings.onCancel.call(this);
1060
+ }
1061
+
1062
+ if (this.settings.submitForm) {
1063
+ this.submitForm();
1064
+ }
1065
+
1066
+ this.close();
1067
+ },
1068
+
1069
+ setValue: function (values) {
1070
+ this.settings.value = values || [];
1071
+ this.renderOptions();
1072
+ this.updateTriggerText();
1073
+ this.updateHiddenInputs();
1074
+ this.updateDataValueAttribute();
1075
+ },
1076
+
1077
+ getValue: function () {
1078
+ return this.getSelectedValues();
1079
+ },
1080
+
1081
+ updateData: function (data) {
1082
+ this.settings.data = data || [];
1083
+ this.settings.value = [];
1084
+ this.renderOptions();
1085
+ this.updateTriggerText();
1086
+ this.updateHiddenInputs();
1087
+ this.updateDataValueAttribute();
1088
+ },
1089
+
1090
+ loadData: function (customAjaxConfig, onSuccess, onError, appendData) {
1091
+ var self = this;
1092
+ var ajaxConfig = customAjaxConfig || this.settings.ajax;
1093
+
1094
+ if (!ajaxConfig || !ajaxConfig.url) {
1095
+ console.error('OneSelect: Ajax configuration or url is missing');
1096
+ return;
1097
+ }
1098
+
1099
+ if (this.settings.beforeLoad) {
1100
+ this.settings.beforeLoad.call(this);
1101
+ }
1102
+
1103
+ if (!appendData) {
1104
+ this.trigger.find('.cms-selected-text').text('Loading...');
1105
+ }
1106
+
1107
+ var request = $.extend(true, {
1108
+ url: ajaxConfig.url,
1109
+ method: ajaxConfig.method || 'GET',
1110
+ data: ajaxConfig.data || {},
1111
+ dataType: ajaxConfig.dataType || 'json',
1112
+ success: function (response) {
1113
+ var data = response;
1114
+ if (typeof response === 'object' && response.data) {
1115
+ data = response.data;
1116
+ } else if (typeof response === 'object' && response.results) {
1117
+ data = response.results;
1118
+ }
1119
+
1120
+ // Handle hasNextPage from response
1121
+ if (response && typeof response.hasNextPage !== 'undefined') {
1122
+ self.hasNextPage = response.hasNextPage;
1123
+ } else {
1124
+ self.hasNextPage = false;
1125
+ }
1126
+
1127
+ // Handle currentPage from response (optional)
1128
+ if (response && typeof response.currentPage !== 'undefined') {
1129
+ self.currentPage = response.currentPage;
1130
+ }
1131
+
1132
+ // Append or replace data
1133
+ if (appendData) {
1134
+ // Merge new data with existing data
1135
+ self.settings.data = $.extend({}, self.settings.data, data || {});
1136
+ self.appendOptions(data || {});
1137
+ } else {
1138
+ self.settings.data = data || [];
1139
+ self.renderOptions();
1140
+ }
1141
+
1142
+ self.updateTriggerText();
1143
+
1144
+ if (self.settings.afterLoad) {
1145
+ self.settings.afterLoad.call(self, data, response);
1146
+ }
1147
+
1148
+ if (onSuccess) {
1149
+ onSuccess.call(self, data, response);
1150
+ }
1151
+ },
1152
+ error: function (xhr, status, error) {
1153
+ if (!appendData) {
1154
+ self.trigger.find('.cms-selected-text').text('Error loading data');
1155
+ }
1156
+
1157
+ if (self.settings.onLoadError) {
1158
+ self.settings.onLoadError.call(self, xhr, status, error);
1159
+ }
1160
+
1161
+ if (onError) {
1162
+ onError.call(self, xhr, status, error);
1163
+ }
1164
+ }
1165
+ }, ajaxConfig);
1166
+
1167
+ if (ajaxConfig.success) {
1168
+ var originalSuccess = request.success;
1169
+ request.success = function (response) {
1170
+ ajaxConfig.success(response);
1171
+ originalSuccess(response);
1172
+ };
1173
+ }
1174
+
1175
+ if (ajaxConfig.error) {
1176
+ var originalError = request.error;
1177
+ request.error = function (xhr, status, error) {
1178
+ ajaxConfig.error(xhr, status, error);
1179
+ originalError(xhr, status, error);
1180
+ };
1181
+ }
1182
+
1183
+ $.ajax(request);
1184
+ },
1185
+
1186
+ loadNextPage: function () {
1187
+ var self = this;
1188
+ this.settings.loading = true;
1189
+ this.preloader.show();
1190
+
1191
+ var ajaxConfig = $.extend(true, {}, this.settings.ajax);
1192
+ ajaxConfig.data = ajaxConfig.data || {};
1193
+ ajaxConfig.data.page = this.currentPage + 1;
1194
+
1195
+ this.loadData(ajaxConfig, function (data, response) {
1196
+ self.settings.loading = false;
1197
+ self.preloader.hide();
1198
+
1199
+ if (!response.currentPage) {
1200
+ self.currentPage++;
1201
+ }
1202
+ }, function () {
1203
+ self.settings.loading = false;
1204
+ self.preloader.hide();
1205
+ }, true);
1206
+ },
1207
+
1208
+ reload: function () {
1209
+ if (this.settings.ajax) {
1210
+ this.loadData();
1211
+ } else {
1212
+ console.warn('OneSelect: No ajax configuration found');
1213
+ }
1214
+ },
1215
+
1216
+ select: function (value) {
1217
+ var self = this;
1218
+ var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + this.htmlEncode(value) + '"]');
1219
+
1220
+ if (checkbox.length) {
1221
+ checkbox.prop('checked', true);
1222
+ checkbox.closest('.cms-option').addClass('selected');
1223
+ }
1224
+
1225
+ if (Array.isArray(this.settings.value)) {
1226
+ if (!this.isValueSelected(value)) {
1227
+ this.settings.value.push(value);
1228
+ }
1229
+ }
1230
+
1231
+ this.updateSelectAllState();
1232
+ this.updateTriggerText();
1233
+ this.updateHiddenInputs();
1234
+ this.updateDataValueAttribute();
1235
+ },
1236
+
1237
+ unselect: function (value) {
1238
+ var self = this;
1239
+ var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + this.htmlEncode(value) + '"]');
1240
+
1241
+ if (checkbox.length) {
1242
+ checkbox.prop('checked', false);
1243
+ checkbox.closest('.cms-option').removeClass('selected');
1244
+ }
1245
+
1246
+ if (Array.isArray(this.settings.value)) {
1247
+ var strValue = String(value).trim();
1248
+ this.settings.value = this.settings.value.filter(function (v) {
1249
+ return String(v).trim() !== strValue;
1250
+ });
1251
+ }
1252
+
1253
+ this.updateSelectAllState();
1254
+ this.updateTriggerText();
1255
+ this.updateHiddenInputs();
1256
+ this.updateDataValueAttribute();
1257
+ },
1258
+
1259
+ selectAll: function () {
1260
+ this.handleSelectAll(true);
1261
+ },
1262
+
1263
+ unselectAll: function () {
1264
+ this.handleSelectAll(false);
1265
+ },
1266
+
1267
+ toggleSelection: function (value) {
1268
+ var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + this.htmlEncode(value) + '"]');
1269
+ if (checkbox.length) {
1270
+ var isChecked = checkbox.prop('checked');
1271
+ checkbox.prop('checked', !isChecked);
1272
+
1273
+ var option = checkbox.closest('.cms-option');
1274
+ if (!isChecked) {
1275
+ option.addClass('selected');
1276
+ if (Array.isArray(this.settings.value)) {
1277
+ if (!this.isValueSelected(value)) {
1278
+ this.settings.value.push(value);
1279
+ }
1280
+ }
1281
+ } else {
1282
+ option.removeClass('selected');
1283
+ if (Array.isArray(this.settings.value)) {
1284
+ var strValue = String(value).trim();
1285
+ this.settings.value = this.settings.value.filter(function (v) { return String(v).trim() !== strValue });
1286
+ }
1287
+ }
1288
+
1289
+ this.updateSelectAllState();
1290
+ this.updateTriggerText();
1291
+ this.updateHiddenInputs();
1292
+ }
1293
+ },
1294
+
1295
+ getInstanceId: function () {
1296
+ return this.instanceId;
1297
+ },
1298
+
1299
+ destroy: function () {
1300
+ delete instances[this.instanceId];
1301
+
1302
+ $(window).off('.cms');
1303
+ $(window).off('.ones');
1304
+ $(document).off('.onescroll');
1305
+ this.trigger.off();
1306
+ this.okBtn.off();
1307
+ this.cancelBtn.off();
1308
+ this.optionsContainer.off();
1309
+
1310
+ // Clear horizontal scroll tracking interval
1311
+ if (this._horizontalScrollInterval) {
1312
+ clearInterval(this._horizontalScrollInterval);
1313
+ this._horizontalScrollInterval = null;
1314
+ }
1315
+
1316
+ $('input.cms-hidden-input[data-cms-input="' + this.settings.name + '"]').remove();
1317
+
1318
+ this.wrapper.remove();
1319
+ this.dropdown.remove();
1320
+ this.$element.removeData(pluginName);
1321
+ }
1322
+ };
1323
+
1324
+ /**
1325
+ * Get instance by ID
1326
+ */
1327
+ OneSelect.getInstance = function (instanceId) {
1328
+ return instances[instanceId] || null;
1329
+ };
1330
+
1331
+ /**
1332
+ * Get all instances
1333
+ */
1334
+ OneSelect.getAllInstances = function () {
1335
+ return instances;
1336
+ };
1337
+
1338
+ /**
1339
+ * jQuery Plugin Registration
1340
+ */
1341
+ $.fn[pluginName] = function (options) {
1342
+ var args = arguments;
1343
+ var returnValue = this;
1344
+
1345
+ this.each(function () {
1346
+ var $this = $(this);
1347
+ var instance = $this.data(pluginName);
1348
+
1349
+ // Initialize if not already
1350
+ if (!instance) {
1351
+ // Don't auto-initialize - only init if options are provided
1352
+ if (typeof options === 'object' || !options) {
1353
+ instance = new OneSelect(this, options);
1354
+ $this.data(pluginName, instance);
1355
+ } else {
1356
+ // Method called without initialization
1357
+ return;
1358
+ }
1359
+ }
1360
+
1361
+ // Call method
1362
+ if (typeof options === 'string') {
1363
+ if (options === 'value') {
1364
+ if (args[1] !== undefined) {
1365
+ instance.setValue(args[1]);
1366
+ returnValue = $this;
1367
+ } else {
1368
+ returnValue = instance.getValue();
1369
+ }
1370
+ } else if (options === 'getValues') {
1371
+ returnValue = instance.getSelectedValues();
1372
+ } else if (options === 'getLabels') {
1373
+ returnValue = instance.getSelectedLabels();
1374
+ } else if (options === 'getInstanceId') {
1375
+ returnValue = instance.getInstanceId();
1376
+ } else if (options === 'updateData') {
1377
+ instance.updateData(args[1]);
1378
+ } else if (options === 'loadData') {
1379
+ instance.loadData(args[1], args[2], args[3]);
1380
+ } else if (options === 'reload') {
1381
+ instance.reload();
1382
+ } else if (options === 'select') {
1383
+ instance.select(args[1]);
1384
+ } else if (options === 'unselect') {
1385
+ instance.unselect(args[1]);
1386
+ } else if (options === 'selectAll') {
1387
+ instance.selectAll();
1388
+ } else if (options === 'unselectAll') {
1389
+ instance.unselectAll();
1390
+ } else if (options === 'toggleSelection') {
1391
+ instance.toggleSelection(args[1]);
1392
+ } else if (options === 'open') {
1393
+ instance.open();
1394
+ } else if (options === 'close') {
1395
+ instance.close();
1396
+ } else if (options === 'destroy') {
1397
+ instance.destroy();
1398
+ }
1399
+ }
1400
+ });
1401
+
1402
+ return returnValue;
1403
+ };
1404
+
1405
+ // Expose constructor
1406
+ $.fn[pluginName].Constructor = OneSelect;
1407
+
1408
+ // Expose static methods
1409
+ $.fn[pluginName].getInstance = OneSelect.getInstance;
1410
+ $.fn[pluginName].getAllInstances = OneSelect.getAllInstances;
1411
+
1412
+ }));