@kamranbaylarov/one-select 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1227 @@
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
+ selectedValues: [],
91
+ value: null, // Single value or array of values to pre-select
92
+ valueField: 'value',
93
+ labelField: 'label',
94
+ showCheckbox: true,
95
+ showBadges: false,
96
+ showBadgesExternal: null,
97
+ showSearch: false, // Show search input in dropdown
98
+ searchPlaceholder: 'Search...',
99
+ searchUrl: null, // URL for AJAX search (GET request)
100
+ searchDebounceDelay: 300,// Delay in milliseconds for search debounce
101
+ closeOnScroll: false,
102
+ closeOnOutside: true, // Close dropdown when clicking outside (default: true)
103
+ submitForm: false,
104
+ submitOnOutside: false,
105
+ formId: null,
106
+ name: null,
107
+ multiple: true,
108
+ ajax: null,
109
+ autoLoad: true,
110
+ beforeLoad: null,
111
+ afterLoad: null,
112
+ onLoadError: null,
113
+ onChange: null,
114
+ onSelect: null,
115
+ onOk: null,
116
+ onCancel: null
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-selected': 'selectedValues',
137
+ 'ones-value': 'value',
138
+ 'ones-value-field': 'valueField',
139
+ 'ones-label-field': 'labelField',
140
+ 'ones-name': 'name',
141
+ 'ones-multiple': 'multiple',
142
+ 'ones-show-checkbox': 'showCheckbox',
143
+ 'ones-show-badges': 'showBadges',
144
+ 'ones-show-badges-external': 'showBadgesExternal',
145
+ 'ones-show-search': 'showSearch',
146
+ 'ones-search-placeholder': 'searchPlaceholder',
147
+ 'ones-search-url': 'searchUrl',
148
+ 'ones-search-debounce-delay': 'searchDebounceDelay',
149
+ 'ones-close-on-scroll': 'closeOnScroll',
150
+ 'ones-close-on-outside': 'closeOnOutside',
151
+ 'ones-submit-form': 'submitForm',
152
+ 'ones-submit-on-outside': 'submitOnOutside',
153
+ 'ones-form-id': 'formId',
154
+ 'ones-auto-load': 'autoLoad'
155
+ };
156
+
157
+ $.each(attributeMap, function(attr, setting) {
158
+ var value = self.$element.data(attr);
159
+
160
+ if (value === undefined) {
161
+ return;
162
+ }
163
+
164
+ if (setting === 'data' || setting === 'selectedValues' || setting === 'value') {
165
+ if (typeof value === 'string') {
166
+ try {
167
+ dataOptions[setting] = JSON.parse(value);
168
+ } catch (e) {
169
+ console.warn('OneSelect: Invalid JSON for ' + attr, value);
170
+ dataOptions[setting] = value;
171
+ }
172
+ } else {
173
+ dataOptions[setting] = value;
174
+ }
175
+ } else if (setting === 'multiple' || setting === 'showCheckbox' ||
176
+ setting === 'showBadges' || setting === 'showSearch' ||
177
+ setting === 'closeOnScroll' || setting === 'closeOnOutside' ||
178
+ setting === 'submitForm' || setting === 'submitOnOutside' ||
179
+ setting === 'autoLoad') {
180
+ if (typeof value === 'string') {
181
+ dataOptions[setting] = value === 'true' || value === '1';
182
+ } else {
183
+ dataOptions[setting] = !!value;
184
+ }
185
+ } else if (setting === 'searchDebounceDelay') {
186
+ // Parse as number
187
+ if (typeof value === 'string') {
188
+ dataOptions[setting] = parseInt(value, 10) || 300;
189
+ } else {
190
+ dataOptions[setting] = value || 300;
191
+ }
192
+ } else {
193
+ dataOptions[setting] = value;
194
+ }
195
+ });
196
+
197
+ var ajaxData = this.$element.data('ones-ajax');
198
+ if (ajaxData) {
199
+ if (typeof ajaxData === 'string') {
200
+ try {
201
+ dataOptions.ajax = JSON.parse(ajaxData);
202
+ } catch (e) {
203
+ console.warn('OneSelect: Invalid JSON for ones-ajax', ajaxData);
204
+ }
205
+ } else {
206
+ dataOptions.ajax = ajaxData;
207
+ }
208
+ }
209
+
210
+ return dataOptions;
211
+ },
212
+
213
+ init: function() {
214
+ // Register instance in global registry
215
+ instances[this.instanceId] = this;
216
+
217
+ // Merge value parameter into selectedValues
218
+ this.settings.selectedValues = this.mergeValueSettings(this.settings.value, this.settings.selectedValues);
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
+ /**
236
+ * Merge value parameter into selectedValues array
237
+ * @param {*} value - Single value or array of values
238
+ * @param {Array} selectedValues - Existing selected values
239
+ * @returns {Array} Merged array of selected values
240
+ */
241
+ mergeValueSettings: function(value, selectedValues) {
242
+ var result = selectedValues ? [].concat(selectedValues) : [];
243
+
244
+ if (value !== null && value !== undefined) {
245
+ if (Array.isArray(value)) {
246
+ // value is an array - merge all items
247
+ $.each(value, function(i, v) {
248
+ if ($.inArray(v, result) === -1) {
249
+ result.push(v);
250
+ }
251
+ });
252
+ } else {
253
+ // value is a single value - add if not already present
254
+ if ($.inArray(value, result) === -1) {
255
+ result.push(value);
256
+ }
257
+ }
258
+ }
259
+
260
+ return result;
261
+ },
262
+
263
+ build: function() {
264
+ // Add search input at the top of dropdown if enabled
265
+ if (this.settings.showSearch) {
266
+ this.dropdown.append(this.searchInput);
267
+ }
268
+ this.dropdown.append(this.optionsContainer);
269
+ this.dropdown.append(this.buttons);
270
+ this.wrapper.append(this.trigger);
271
+
272
+ // Append wrapper to $element, dropdown to body
273
+ this.$element.append(this.wrapper);
274
+ $('body').append(this.dropdown);
275
+
276
+ this.renderOptions();
277
+ this.updateTriggerText();
278
+ this.updateHiddenInputs();
279
+ },
280
+
281
+ updateHiddenInputs: function() {
282
+ if (!this.settings.name) {
283
+ return;
284
+ }
285
+
286
+ var form = null;
287
+ if (this.settings.formId) {
288
+ form = $('#' + this.settings.formId);
289
+ } else {
290
+ form = this.$element.closest('form');
291
+ }
292
+
293
+ var container = form.length ? form : this.wrapper;
294
+
295
+ container.find('input.cms-hidden-input[data-cms-input="' + this.settings.name + '"]').remove();
296
+
297
+ var inputName = this.settings.name;
298
+ if (this.settings.multiple && inputName.indexOf('[') === -1) {
299
+ inputName += '[]';
300
+ }
301
+
302
+ var selectedValues = this.getSelectedValues();
303
+
304
+ if (this.settings.multiple) {
305
+ $.each(selectedValues, function(index, value) {
306
+ var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
307
+ .attr('name', inputName)
308
+ .attr('value', value)
309
+ .attr('data-cms-input', this.settings.name)
310
+ .attr('data-cms-value', value);
311
+ container.append(hiddenInput);
312
+ }.bind(this));
313
+ } else {
314
+ var value = selectedValues.length > 0 ? selectedValues.join(',') : '';
315
+ var hiddenInput = $('<input type="hidden" class="cms-hidden-input">')
316
+ .attr('name', inputName)
317
+ .attr('value', value)
318
+ .attr('data-cms-input', this.settings.name);
319
+ container.append(hiddenInput);
320
+ }
321
+ },
322
+
323
+ createWrapper: function() {
324
+ return $('<div class="cms-wrapper"></div>');
325
+ },
326
+
327
+ createTrigger: function() {
328
+ return $('<div class="cms-trigger"><span class="cms-selected-text cms-placeholder">' +
329
+ this.settings.placeholder + '</span></div>');
330
+ },
331
+
332
+ createDropdown: function() {
333
+ return $('<div class="cms-dropdown"></div>');
334
+ },
335
+
336
+ createSearchInput: function() {
337
+ return $('<div class="cms-search-wrapper">' +
338
+ '<input type="text" class="cms-search-input" placeholder="' +
339
+ this.settings.searchPlaceholder + '" /></div>');
340
+ },
341
+
342
+ createOptionsContainer: function() {
343
+ return $('<div class="cms-options-container"></div>');
344
+ },
345
+
346
+ createButtons: function() {
347
+ var container = $('<div class="cms-buttons"></div>');
348
+ this.okBtn = $('<button class="cms-btn cms-btn-ok">' + this.settings.okText + '</button>');
349
+ this.cancelBtn = $('<button class="cms-btn cms-btn-cancel">' + this.settings.cancelText + '</button>');
350
+
351
+ container.append(this.okBtn);
352
+ container.append(this.cancelBtn);
353
+
354
+ return container;
355
+ },
356
+
357
+ renderOptions: function() {
358
+ this.optionsContainer.empty();
359
+
360
+ var selectAllOption = this.createOption('select-all', this.settings.selectAllText, false);
361
+ this.optionsContainer.append(selectAllOption);
362
+
363
+ var self = this;
364
+ $.each(this.settings.data, function(index, item) {
365
+ var value, label;
366
+
367
+ if (typeof item === 'object') {
368
+ value = item[self.settings.valueField];
369
+ label = item[self.settings.labelField];
370
+ } else {
371
+ value = item;
372
+ label = item;
373
+ }
374
+
375
+ var isSelected = $.inArray(value, self.settings.selectedValues) !== -1;
376
+ var option = self.createOption(value, label, isSelected);
377
+ self.optionsContainer.append(option);
378
+ });
379
+
380
+ this.updateSelectAllState();
381
+ },
382
+
383
+ createOption: function(value, label, checked) {
384
+ var optionClass = 'cms-option';
385
+ if (!this.settings.showCheckbox) {
386
+ optionClass += ' cms-hide-checkbox';
387
+ }
388
+
389
+ if (checked && value !== 'select-all') {
390
+ optionClass += ' selected';
391
+ }
392
+
393
+ var option = $('<div class="' + optionClass + '" data-value="' + value + '"></div>');
394
+ var checkbox = $('<input type="checkbox" value="' + value + '"' +
395
+ (checked ? ' checked' : '') + '>');
396
+ var labelEl = $('<label>' + label + '</label>');
397
+
398
+ option.append(checkbox);
399
+ option.append(labelEl);
400
+
401
+ return option;
402
+ },
403
+
404
+ /**
405
+ * Filter options based on search text
406
+ * @param {String} searchText - Search text to filter by
407
+ */
408
+ filterOptions: function(searchText) {
409
+ var self = this;
410
+ var options = this.optionsContainer.find('.cms-option:not([data-value="select-all"])');
411
+
412
+ if (searchText === '') {
413
+ // Show all options if search is empty
414
+ options.show();
415
+ } else {
416
+ // Filter options by label
417
+ options.each(function() {
418
+ var option = $(this);
419
+ var label = option.find('label').text().toLowerCase();
420
+
421
+ if (label.indexOf(searchText) !== -1) {
422
+ option.show();
423
+ } else {
424
+ option.hide();
425
+ }
426
+ });
427
+ }
428
+ },
429
+
430
+ /**
431
+ * Perform AJAX search
432
+ * @param {String} searchText - Search text to send to server
433
+ */
434
+ performAjaxSearch: function(searchText) {
435
+ var self = this;
436
+
437
+ // Show loading state
438
+ this.optionsContainer.addClass('cms-loading');
439
+
440
+ $.ajax({
441
+ url: this.settings.searchUrl,
442
+ method: 'GET',
443
+ data: { q: searchText },
444
+ dataType: 'json',
445
+ success: function(response) {
446
+ // Handle different response formats
447
+ var data = response;
448
+ if (typeof response === 'object' && response.data) {
449
+ data = response.data;
450
+ } else if (typeof response === 'object' && response.results) {
451
+ data = response.results;
452
+ }
453
+
454
+ // Update dropdown with search results
455
+ self.updateSearchResults(data || []);
456
+
457
+ self.optionsContainer.removeClass('cms-loading');
458
+ },
459
+ error: function(xhr, status, error) {
460
+ console.error('OneSelect: Search error', error);
461
+ self.optionsContainer.removeClass('cms-loading');
462
+
463
+ if (self.settings.onLoadError) {
464
+ self.settings.onLoadError.call(self, xhr, status, error);
465
+ }
466
+ }
467
+ });
468
+ },
469
+
470
+ /**
471
+ * Update dropdown with search results
472
+ * @param {Array} data - Search results data
473
+ */
474
+ updateSearchResults: function(data) {
475
+ // Clear existing options (except select-all)
476
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"])').remove();
477
+
478
+ var self = this;
479
+ $.each(data, function(index, item) {
480
+ var value, label;
481
+
482
+ if (typeof item === 'object') {
483
+ value = item[self.settings.valueField];
484
+ label = item[self.settings.labelField];
485
+ } else {
486
+ value = item;
487
+ label = item;
488
+ }
489
+
490
+ // Keep selection state if value was previously selected
491
+ var isSelected = $.inArray(value, self.settings.selectedValues) !== -1;
492
+ var option = self.createOption(value, label, isSelected);
493
+ self.optionsContainer.append(option);
494
+ });
495
+
496
+ // Update select-all state
497
+ this.updateSelectAllState();
498
+ },
499
+
500
+ attachEvents: function() {
501
+ var self = this;
502
+
503
+ this.trigger.on('click', function(e) {
504
+ e.stopPropagation();
505
+ self.toggle();
506
+ });
507
+
508
+ // Search input event listener
509
+ if (this.settings.showSearch) {
510
+ if (this.settings.searchUrl) {
511
+ // AJAX search with debounce
512
+ var debouncedSearch = debounce(function(searchText) {
513
+ self.performAjaxSearch(searchText);
514
+ }, this.settings.searchDebounceDelay);
515
+
516
+ this.searchInput.find('.cms-search-input').on('keyup', function() {
517
+ var searchText = $(this).val();
518
+ if (searchText.length > 0) {
519
+ debouncedSearch(searchText);
520
+ } else {
521
+ // Show original data when search is empty
522
+ self.filterOptions('');
523
+ }
524
+ });
525
+ } else {
526
+ // Local filtering (default)
527
+ this.searchInput.find('.cms-search-input').on('keyup', function() {
528
+ var searchText = $(this).val().toLowerCase();
529
+ self.filterOptions(searchText);
530
+ });
531
+ }
532
+ }
533
+
534
+ $(window).on('resize.cms', function() {
535
+ if (self.wrapper.hasClass('open')) {
536
+ self.updateDropdownPosition();
537
+ }
538
+ });
539
+
540
+ if (this.settings.closeOnScroll) {
541
+ $(window).on('scroll.cms', function() {
542
+ if (self.wrapper.hasClass('open')) {
543
+ self.close();
544
+ }
545
+ });
546
+ } else {
547
+ // Update dropdown position on vertical scroll
548
+ $(window).on('scroll.cms', function() {
549
+ if (self.wrapper.hasClass('open')) {
550
+ self.updateDropdownPosition();
551
+ }
552
+ });
553
+ }
554
+
555
+ // Global horizontal scroll handler - close dropdown on any horizontal scroll
556
+ // Listen for wheel events with horizontal delta
557
+ $(document).on('wheel.onescroll', function(e) {
558
+ if (!self.wrapper.hasClass('open')) {
559
+ return;
560
+ }
561
+
562
+ // Check if horizontal scrolling (deltaX != 0)
563
+ if (e.originalEvent && Math.abs(e.originalEvent.deltaX) > 0) {
564
+ self.close();
565
+ }
566
+ });
567
+
568
+ // Also listen for scroll events on all elements to detect horizontal scroll
569
+ // Using MutationObserver to detect when elements with overflow scroll
570
+ self._detectHorizontalScroll = function() {
571
+ if (!self.wrapper.hasClass('open')) return;
572
+
573
+ // Check window horizontal scroll
574
+ if (window.scrollX !== self._lastWindowScrollX) {
575
+ self._lastWindowScrollX = window.scrollX;
576
+ if (self._lastWindowScrollX > 0) {
577
+ self.close();
578
+ return;
579
+ }
580
+ }
581
+
582
+ // Check all scrollable elements for horizontal scroll
583
+ var scrollableElements = document.querySelectorAll('*');
584
+ for (var i = 0; i < scrollableElements.length; i++) {
585
+ var el = scrollableElements[i];
586
+ var key = getElementKey(el);
587
+
588
+ if (self._elementScrollPositions[key] !== undefined) {
589
+ var currentScroll = el.scrollLeft;
590
+ if (currentScroll !== self._elementScrollPositions[key]) {
591
+ // Horizontal scroll detected
592
+ self.close();
593
+ return;
594
+ }
595
+ }
596
+ }
597
+ };
598
+
599
+ // Store scroll positions and track changes
600
+ self._elementScrollPositions = {};
601
+ self._lastWindowScrollX = window.scrollX;
602
+
603
+ function getElementKey(el) {
604
+ if (el === document) return 'document';
605
+ if (el === document.documentElement) return 'html';
606
+ if (el === document.body) return 'body';
607
+ return el.tagName + '-' + (el.id || el.className || Math.random().toString(36).substr(2, 9));
608
+ }
609
+
610
+ // Override open to initialize tracking
611
+ var originalOpen = self.open.bind(self);
612
+ self.open = function() {
613
+ // Store initial scroll positions
614
+ self._elementScrollPositions = {};
615
+ self._lastWindowScrollX = window.scrollX;
616
+
617
+ var scrollableElements = document.querySelectorAll('*');
618
+ for (var i = 0; i < scrollableElements.length; i++) {
619
+ var el = scrollableElements[i];
620
+ if (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight) {
621
+ self._elementScrollPositions[getElementKey(el)] = el.scrollLeft;
622
+ }
623
+ }
624
+
625
+ // Start checking periodically
626
+ if (self._horizontalScrollInterval) {
627
+ clearInterval(self._horizontalScrollInterval);
628
+ }
629
+ self._horizontalScrollInterval = setInterval(function() {
630
+ self._detectHorizontalScroll();
631
+ }, 50);
632
+
633
+ originalOpen();
634
+ };
635
+
636
+ // Override close to stop tracking
637
+ var originalClose = self.close.bind(self);
638
+ self.close = function() {
639
+ if (self._horizontalScrollInterval) {
640
+ clearInterval(self._horizontalScrollInterval);
641
+ self._horizontalScrollInterval = null;
642
+ }
643
+ self._elementScrollPositions = {};
644
+ originalClose();
645
+ };
646
+
647
+ // Window click handler - close dropdown when clicking outside
648
+ $(window).on('click.ones', function(e) {
649
+ if (!self.settings.closeOnOutside) {
650
+ return;
651
+ }
652
+
653
+ if (!self.wrapper.hasClass('open')) {
654
+ return;
655
+ }
656
+
657
+ var $target = $(e.target);
658
+ // Don't close if clicked inside dropdown or wrapper elements
659
+ if ($target.closest('.cms-wrapper').length > 0 ||
660
+ $target.closest('.cms-dropdown').length > 0) {
661
+ return;
662
+ }
663
+
664
+ // Submit form if submitOnOutside is enabled
665
+ if (self.settings.submitOnOutside) {
666
+ self.updateTriggerText();
667
+ if (self.settings.onOk) {
668
+ self.settings.onOk.call(self, self.getSelectedValues(), self.getSelectedLabels());
669
+ }
670
+ self.submitForm();
671
+ }
672
+
673
+ self.close();
674
+ });
675
+
676
+ this.optionsContainer.on('click', '.cms-option', function(e) {
677
+ e.stopPropagation();
678
+
679
+ var option = $(this);
680
+ var checkbox = option.find('input[type="checkbox"]');
681
+
682
+ if ($(e.target).is('input[type="checkbox"]')) {
683
+ return;
684
+ }
685
+
686
+ checkbox.prop('checked', !checkbox.prop('checked'));
687
+ self.handleOptionChange(option);
688
+ });
689
+
690
+ this.optionsContainer.on('change', 'input[type="checkbox"]', function(e) {
691
+ e.stopPropagation();
692
+ var option = $(e.target).closest('.cms-option');
693
+ self.handleOptionChange(option);
694
+ });
695
+
696
+ this.okBtn.on('click', function(e) {
697
+ e.stopPropagation();
698
+ self.handleOk();
699
+ });
700
+
701
+ this.cancelBtn.on('click', function(e) {
702
+ e.stopPropagation();
703
+ self.handleCancel();
704
+ });
705
+ },
706
+
707
+ handleOptionChange: function(option) {
708
+ var value = option.data('value');
709
+
710
+ if (value === 'select-all') {
711
+ var checkbox = option.find('input[type="checkbox"]');
712
+ this.handleSelectAll(checkbox.prop('checked'));
713
+ } else {
714
+ var checkbox = option.find('input[type="checkbox"]');
715
+ if (checkbox.prop('checked')) {
716
+ option.addClass('selected');
717
+ } else {
718
+ option.removeClass('selected');
719
+ }
720
+
721
+ var self = this;
722
+ setTimeout(function() {
723
+ self.updateSelectAllState();
724
+ self.updateTriggerText();
725
+ }, 0);
726
+ }
727
+
728
+ this.updateHiddenInputs();
729
+
730
+ if (this.settings.onChange) {
731
+ this.settings.onChange.call(this, this.getSelectedValues(), this.getSelectedLabels());
732
+ }
733
+
734
+ if (this.settings.onSelect) {
735
+ this.settings.onSelect.call(this, this.getSelectedValues());
736
+ }
737
+ },
738
+
739
+ handleSelectAll: function(checked) {
740
+ var self = this;
741
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"])').each(function() {
742
+ var option = $(this);
743
+ option.find('input[type="checkbox"]').prop('checked', checked);
744
+ if (checked) {
745
+ option.addClass('selected');
746
+ } else {
747
+ option.removeClass('selected');
748
+ }
749
+ });
750
+
751
+ this.updateSelectAllState();
752
+ this.updateTriggerText();
753
+ this.updateHiddenInputs();
754
+ },
755
+
756
+ updateSelectAllState: function() {
757
+ var allOptions = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]');
758
+ var checkedOptions = allOptions.filter(':checked');
759
+ var totalCount = allOptions.length;
760
+ var checkedCount = checkedOptions.length;
761
+
762
+ var selectAllCheckbox = this.optionsContainer.find('.cms-option[data-value="select-all"] input[type="checkbox"]');
763
+
764
+ selectAllCheckbox.prop('indeterminate', false);
765
+ selectAllCheckbox.prop('checked', false);
766
+
767
+ if (checkedCount === 0) {
768
+ // Nothing selected
769
+ } else if (checkedCount === totalCount && totalCount > 0) {
770
+ selectAllCheckbox.prop('checked', true);
771
+ } else {
772
+ selectAllCheckbox.prop('indeterminate', true);
773
+ }
774
+ },
775
+
776
+ getSelectedValues: function() {
777
+ var values = [];
778
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]:checked')
779
+ .each(function() {
780
+ var val = $(this).val();
781
+ if (!isNaN(val) && val !== '') {
782
+ val = Number(val);
783
+ }
784
+ values.push(val);
785
+ });
786
+ return values;
787
+ },
788
+
789
+ getSelectedLabels: function() {
790
+ var labels = [];
791
+ this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"]:checked')
792
+ .siblings('label')
793
+ .each(function() {
794
+ labels.push($(this).text());
795
+ });
796
+ return labels;
797
+ },
798
+
799
+ updateTriggerText: function() {
800
+ var labels = this.getSelectedLabels();
801
+ var values = this.getSelectedValues();
802
+ var textSpan = this.trigger.find('.cms-selected-text');
803
+
804
+ if (labels.length === 0) {
805
+ textSpan.empty().text(this.settings.placeholder).addClass('cms-placeholder');
806
+ } else if (this.settings.showBadges) {
807
+ textSpan.empty().removeClass('cms-placeholder');
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.stopPropagation();
817
+ self.unselect(value);
818
+ });
819
+
820
+ badge.append(labelSpan);
821
+ badge.append(removeBtn);
822
+ textSpan.append(badge);
823
+ });
824
+ } else {
825
+ textSpan.empty().removeClass('cms-placeholder');
826
+ textSpan.text(labels.length + ' items selected');
827
+ }
828
+
829
+ this.updateExternalBadges(values, labels);
830
+ },
831
+
832
+ updateExternalBadges: function(values, labels) {
833
+ if (!this.settings.showBadgesExternal) {
834
+ return;
835
+ }
836
+
837
+ var $externalContainer = $('#' + this.settings.showBadgesExternal);
838
+
839
+ if ($externalContainer.length === 0) {
840
+ console.warn('OneSelect: External container not found - #' + this.settings.showBadgesExternal);
841
+ return;
842
+ }
843
+
844
+ $externalContainer.empty();
845
+
846
+ if (values.length === 0) {
847
+ return;
848
+ }
849
+
850
+ var self = this;
851
+ $.each(values, function(index, value) {
852
+ var badge = $('<span class="cms-badge"></span>');
853
+ var labelSpan = $('<span></span>').text(labels[index]);
854
+ var removeBtn = $('<button type="button" class="cms-badge-remove">&times;</button>');
855
+
856
+ removeBtn.on('click', function(e) {
857
+ e.preventDefault();
858
+ e.stopPropagation();
859
+ self.unselect(value);
860
+ });
861
+
862
+ badge.append(labelSpan);
863
+ badge.append(removeBtn);
864
+ $externalContainer.append(badge);
865
+ });
866
+ },
867
+
868
+ toggle: function() {
869
+ if (this.wrapper.hasClass('open')) {
870
+ this.close();
871
+ } else {
872
+ this.open();
873
+ }
874
+ },
875
+
876
+ open: function() {
877
+ // Close other open dropdowns
878
+ $('.cms-wrapper.open').not(this.wrapper).removeClass('open');
879
+ $('.cms-dropdown.open').not(this.dropdown).removeClass('open');
880
+
881
+ // Calculate position
882
+ this.updateDropdownPosition();
883
+
884
+ this.wrapper.addClass('open');
885
+ this.dropdown.addClass('open');
886
+ },
887
+
888
+ updateDropdownPosition: function() {
889
+ var rect = this.wrapper[0].getBoundingClientRect();
890
+ var wrapperHeight = this.wrapper.outerHeight();
891
+ var wrapperWidth = this.wrapper.outerWidth();
892
+
893
+ this.dropdown.css({
894
+ position: 'fixed',
895
+ top: rect.bottom + 'px',
896
+ left: rect.left + 'px',
897
+ width: wrapperWidth + 'px'
898
+ });
899
+ },
900
+
901
+ close: function() {
902
+ this.wrapper.removeClass('open');
903
+ this.dropdown.removeClass('open');
904
+ },
905
+
906
+ handleOk: function() {
907
+ this.updateTriggerText();
908
+
909
+ var values = this.getSelectedValues();
910
+ var labels = this.getSelectedLabels();
911
+
912
+ if (this.settings.onOk) {
913
+ this.settings.onOk.call(this, values, labels);
914
+ }
915
+
916
+ if (this.settings.submitForm) {
917
+ this.submitForm();
918
+ }
919
+
920
+ this.close();
921
+ },
922
+
923
+ submitForm: function() {
924
+ var form = null;
925
+
926
+ if (this.settings.formId) {
927
+ form = $('#' + this.settings.formId);
928
+ if (form.length === 0) {
929
+ console.warn('OneSelect: Form with ID "' + this.settings.formId + '" not found');
930
+ return;
931
+ }
932
+ } else {
933
+ form = this.$element.closest('form');
934
+ if (form.length === 0) {
935
+ console.warn('OneSelect: No parent form found');
936
+ return;
937
+ }
938
+ }
939
+
940
+ form[0].submit();
941
+ },
942
+
943
+ handleCancel: function() {
944
+ this.settings.selectedValues = [];
945
+ this.optionsContainer.find('input[type="checkbox"]').prop('checked', false);
946
+ this.optionsContainer.find('.cms-option').removeClass('selected');
947
+ this.updateSelectAllState();
948
+ this.updateTriggerText();
949
+ this.updateHiddenInputs();
950
+
951
+ if (this.settings.onCancel) {
952
+ this.settings.onCancel.call(this);
953
+ }
954
+
955
+ if (this.settings.submitForm) {
956
+ this.submitForm();
957
+ }
958
+
959
+ this.close();
960
+ },
961
+
962
+ setValue: function(values) {
963
+ this.settings.selectedValues = values || [];
964
+ this.renderOptions();
965
+ this.updateTriggerText();
966
+ this.updateHiddenInputs();
967
+ },
968
+
969
+ getValue: function() {
970
+ return this.getSelectedValues();
971
+ },
972
+
973
+ updateData: function(data) {
974
+ this.settings.data = data || [];
975
+ this.settings.selectedValues = [];
976
+ this.renderOptions();
977
+ this.updateTriggerText();
978
+ this.updateHiddenInputs();
979
+ },
980
+
981
+ loadData: function(customAjaxConfig, onSuccess, onError) {
982
+ var self = this;
983
+ var ajaxConfig = customAjaxConfig || this.settings.ajax;
984
+
985
+ if (!ajaxConfig || !ajaxConfig.url) {
986
+ console.error('OneSelect: Ajax configuration or url is missing');
987
+ return;
988
+ }
989
+
990
+ if (this.settings.beforeLoad) {
991
+ this.settings.beforeLoad.call(this);
992
+ }
993
+
994
+ this.trigger.find('.cms-selected-text').text('Loading...');
995
+
996
+ var request = $.extend(true, {
997
+ url: ajaxConfig.url,
998
+ method: ajaxConfig.method || 'GET',
999
+ data: ajaxConfig.data || {},
1000
+ dataType: ajaxConfig.dataType || 'json',
1001
+ success: function(response) {
1002
+ var data = response;
1003
+ if (typeof response === 'object' && response.data) {
1004
+ data = response.data;
1005
+ } else if (typeof response === 'object' && response.results) {
1006
+ data = response.results;
1007
+ }
1008
+
1009
+ self.settings.data = data || [];
1010
+ self.renderOptions();
1011
+ self.updateTriggerText();
1012
+
1013
+ if (self.settings.afterLoad) {
1014
+ self.settings.afterLoad.call(self, data, response);
1015
+ }
1016
+
1017
+ if (onSuccess) {
1018
+ onSuccess.call(self, data, response);
1019
+ }
1020
+ },
1021
+ error: function(xhr, status, error) {
1022
+ self.trigger.find('.cms-selected-text').text('Error loading data');
1023
+
1024
+ if (self.settings.onLoadError) {
1025
+ self.settings.onLoadError.call(self, xhr, status, error);
1026
+ }
1027
+
1028
+ if (onError) {
1029
+ onError.call(self, xhr, status, error);
1030
+ }
1031
+ }
1032
+ }, ajaxConfig);
1033
+
1034
+ if (ajaxConfig.success) {
1035
+ var originalSuccess = request.success;
1036
+ request.success = function(response) {
1037
+ ajaxConfig.success(response);
1038
+ originalSuccess(response);
1039
+ };
1040
+ }
1041
+
1042
+ if (ajaxConfig.error) {
1043
+ var originalError = request.error;
1044
+ request.error = function(xhr, status, error) {
1045
+ ajaxConfig.error(xhr, status, error);
1046
+ originalError(xhr, status, error);
1047
+ };
1048
+ }
1049
+
1050
+ $.ajax(request);
1051
+ },
1052
+
1053
+ reload: function() {
1054
+ if (this.settings.ajax) {
1055
+ this.loadData();
1056
+ } else {
1057
+ console.warn('OneSelect: No ajax configuration found');
1058
+ }
1059
+ },
1060
+
1061
+ select: function(value) {
1062
+ var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + value + '"]');
1063
+ if (checkbox.length) {
1064
+ checkbox.prop('checked', true);
1065
+ checkbox.closest('.cms-option').addClass('selected');
1066
+ this.updateSelectAllState();
1067
+ this.updateTriggerText();
1068
+ this.updateHiddenInputs();
1069
+ }
1070
+ },
1071
+
1072
+ unselect: function(value) {
1073
+ var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + value + '"]');
1074
+ if (checkbox.length) {
1075
+ checkbox.prop('checked', false);
1076
+ checkbox.closest('.cms-option').removeClass('selected');
1077
+ this.updateSelectAllState();
1078
+ this.updateTriggerText();
1079
+ this.updateHiddenInputs();
1080
+ }
1081
+ },
1082
+
1083
+ selectAll: function() {
1084
+ this.handleSelectAll(true);
1085
+ },
1086
+
1087
+ unselectAll: function() {
1088
+ this.handleSelectAll(false);
1089
+ },
1090
+
1091
+ toggleSelection: function(value) {
1092
+ var checkbox = this.optionsContainer.find('.cms-option:not([data-value="select-all"]) input[type="checkbox"][value="' + value + '"]');
1093
+ if (checkbox.length) {
1094
+ var isChecked = checkbox.prop('checked');
1095
+ checkbox.prop('checked', !isChecked);
1096
+
1097
+ var option = checkbox.closest('.cms-option');
1098
+ if (!isChecked) {
1099
+ option.addClass('selected');
1100
+ } else {
1101
+ option.removeClass('selected');
1102
+ }
1103
+
1104
+ this.updateSelectAllState();
1105
+ this.updateTriggerText();
1106
+ this.updateHiddenInputs();
1107
+ }
1108
+ },
1109
+
1110
+ getInstanceId: function() {
1111
+ return this.instanceId;
1112
+ },
1113
+
1114
+ destroy: function() {
1115
+ delete instances[this.instanceId];
1116
+
1117
+ $(window).off('.cms');
1118
+ $(window).off('.ones');
1119
+ $(document).off('.onescroll');
1120
+ this.trigger.off();
1121
+ this.okBtn.off();
1122
+ this.cancelBtn.off();
1123
+ this.optionsContainer.off();
1124
+
1125
+ // Clear horizontal scroll tracking interval
1126
+ if (this._horizontalScrollInterval) {
1127
+ clearInterval(this._horizontalScrollInterval);
1128
+ this._horizontalScrollInterval = null;
1129
+ }
1130
+
1131
+ $('input.cms-hidden-input[data-cms-input="' + this.settings.name + '"]').remove();
1132
+
1133
+ this.wrapper.remove();
1134
+ this.dropdown.remove();
1135
+ this.$element.removeData(pluginName);
1136
+ }
1137
+ };
1138
+
1139
+ /**
1140
+ * Get instance by ID
1141
+ */
1142
+ OneSelect.getInstance = function(instanceId) {
1143
+ return instances[instanceId] || null;
1144
+ };
1145
+
1146
+ /**
1147
+ * Get all instances
1148
+ */
1149
+ OneSelect.getAllInstances = function() {
1150
+ return instances;
1151
+ };
1152
+
1153
+ /**
1154
+ * jQuery Plugin Registration
1155
+ */
1156
+ $.fn[pluginName] = function(options) {
1157
+ var args = arguments;
1158
+ var returnValue = this;
1159
+
1160
+ this.each(function() {
1161
+ var $this = $(this);
1162
+ var instance = $this.data(pluginName);
1163
+
1164
+ // Initialize if not already
1165
+ if (!instance) {
1166
+ // Don't auto-initialize - only init if options are provided
1167
+ if (typeof options === 'object' || !options) {
1168
+ instance = new OneSelect(this, options);
1169
+ $this.data(pluginName, instance);
1170
+ } else {
1171
+ // Method called without initialization
1172
+ return;
1173
+ }
1174
+ }
1175
+
1176
+ // Call method
1177
+ if (typeof options === 'string') {
1178
+ if (options === 'value') {
1179
+ if (args[1] !== undefined) {
1180
+ instance.setValue(args[1]);
1181
+ returnValue = $this;
1182
+ } else {
1183
+ returnValue = instance.getValue();
1184
+ }
1185
+ } else if (options === 'getValues') {
1186
+ returnValue = instance.getSelectedValues();
1187
+ } else if (options === 'getLabels') {
1188
+ returnValue = instance.getSelectedLabels();
1189
+ } else if (options === 'getInstanceId') {
1190
+ returnValue = instance.getInstanceId();
1191
+ } else if (options === 'updateData') {
1192
+ instance.updateData(args[1]);
1193
+ } else if (options === 'loadData') {
1194
+ instance.loadData(args[1], args[2], args[3]);
1195
+ } else if (options === 'reload') {
1196
+ instance.reload();
1197
+ } else if (options === 'select') {
1198
+ instance.select(args[1]);
1199
+ } else if (options === 'unselect') {
1200
+ instance.unselect(args[1]);
1201
+ } else if (options === 'selectAll') {
1202
+ instance.selectAll();
1203
+ } else if (options === 'unselectAll') {
1204
+ instance.unselectAll();
1205
+ } else if (options === 'toggleSelection') {
1206
+ instance.toggleSelection(args[1]);
1207
+ } else if (options === 'open') {
1208
+ instance.open();
1209
+ } else if (options === 'close') {
1210
+ instance.close();
1211
+ } else if (options === 'destroy') {
1212
+ instance.destroy();
1213
+ }
1214
+ }
1215
+ });
1216
+
1217
+ return returnValue;
1218
+ };
1219
+
1220
+ // Expose constructor
1221
+ $.fn[pluginName].Constructor = OneSelect;
1222
+
1223
+ // Expose static methods
1224
+ $.fn[pluginName].getInstance = OneSelect.getInstance;
1225
+ $.fn[pluginName].getAllInstances = OneSelect.getAllInstances;
1226
+
1227
+ }));