PageSourceSearch

https://ctwoodlands.org/wp-content/themes/ctfpa/js/jquery.selectric.js?ver=1.0.0

js ctwoodlands.org collected 2026-10-02 06:22:32 UTC 20,302 bytes, 568 lines download raw bytes

1(function(factory) {
2  /* istanbul ignore next */
3  if (typeof define === 'function' && define.amd) {
4    define(['jquery'], factory);
5  } else if (typeof module === 'object' && module.exports) {
6    // Node/CommonJS
7    module.exports = function( root, jQuery ) {
8      if ( jQuery === undefined ) {
9        if ( typeof window !== 'undefined' ) {
10          jQuery = require('jquery');
11        }
12        else {
13          jQuery = require('jquery')(root);
14        }
15      }
16      factory(jQuery);
17      return jQuery;
18    };
19  } else {
20    // Browser globals
21    factory(jQuery);
22  }
23}(function($) {
24
25  'use strict';
26
27  var pluginName = 'selectric',
28      classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Hover Responsive Above Scroll Group GroupLabel',
29      bindSufix = '.sl',
30      defaults = {
31        onChange: function(elm) { $(elm).change(); },
32        maxHeight: 300,
33        keySearchTimeout: 500,
34        arrowButtonMarkup: '<b class="button">&#x25be;</b>',
35        disableOnMobile: true,
36        openOnHover: false,
37        hoverIntentTimeout: 500,
38        expandToItemText: false,
39        responsive: false,
40        preventWindowScroll: true,
41        inheritOriginalWidth: false,
42        allowWrap: true,
43        customClass: {
44          prefix: pluginName,
45          camelCase: false
46        },
47        optionsItemBuilder: '{text}', // function(itemData, element, index)
48        labelBuilder: '{text}' // function(currItem)
49      },
50      hooks = {
51        add: function(callbackName, hookName, fn) {
52          if ( !this[callbackName] )
53            this[callbackName] = {};
54
55          this[callbackName][hookName] = fn;
56        },
57        remove: function(callbackName, hookName) {
58          delete this[callbackName][hookName];
59        }
60      },
61      _utils = {
62        // Replace diacritics
63        replaceDiacritics: function(s) {
64          // /[\340-\346]/g, // a
65          // /[\350-\353]/g, // e
66          // /[\354-\357]/g, // i
67          // /[\362-\370]/g, // o
68          // /[\371-\374]/g, // u
69          // /[\361]/g,      // n
70          // /[\347]/g,      // c
71          // /[\377]/g       // y
72          var d = '40-46 50-53 54-57 62-70 71-74 61 47 77'.replace(/\d+/g, '\\3$&').split(' '),
73              k = d.length;
74
75          while (k--)
76            s = s.toLowerCase().replace(RegExp('[' + d[k] + ']', 'g'), 'aeiouncy'.charAt(k));
77
78          return s;
79        },
80        // https://gist.github.com/atesgoral/984375
81        format: function(f) {var a=arguments;return(""+f).replace(/{(\d+|(\w+))}/g,function(s,i,p) {return p&&a[1]?a[1][p]:a[i]})},
82        nextEnabledItem: function(selectItems, selected) {
83          while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) {}
84          return selected;
85        },
86        previousEnabledItem: function(selectItems, selected) {
87          while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) {}
88          return selected;
89        },
90        toDash: function(str) {
91          return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
92        },
93        triggerCallback: function(fn, scope) {
94          var elm = scope.element,
95              func = scope.options['on' + fn];
96
97          if ( $.isFunction(func) )
98            func.call(elm, elm, scope);
99
100          if ( hooks[fn] ) {
101            $.each(hooks[fn], function() {
102              this.call(elm, elm, scope);
103            });
104          }
105
106          $(elm).trigger(pluginName + '-' + _utils.toDash(fn), scope);
107        }
108      },
109      $doc = $(document),
110      $win = $(window),
111      Selectric = function(element, opts) {
112        var _this = this,
113            $original = $(element),
114            $input, $items, $itemsScroll, $wrapper, $label, $outerWrapper, $li,
115            isOpen = false,
116            isEnabled = false,
117            selected,
118            currValue,
119            itemsHeight,
120            itemsInnerHeight,
121            finalWidth,
122            optionsLength,
123            eventTriggers,
124            isMobile = /android|ip(hone|od|ad)/i.test(navigator.userAgent),
125            tabindex = $original.prop('tabindex'),
126            labelBuilder;
127
128        function _init(opts) {
129          _this.options = $.extend(true, {}, defaults, _this.options, opts);
130          _this.classes = {};
131          _this.element = element;
132
133          _utils.triggerCallback('BeforeInit', _this);
134
135          // Disable on mobile browsers
136          if ( _this.options.disableOnMobile && isMobile ) {
137            _this.disableOnMobile = true;
138            return;
139          }
140
141          // Preserve data
142          _destroy(true);
143
144          // Generate classNames for elements
145          var customClass   = _this.options.customClass,
146              postfixes     = classList.split(' '),
147              originalWidth = $original.width();
148
149          $.each(postfixes, function(i, currClass) {
150            var c = customClass.prefix + currClass;
151            _this.classes[currClass.toLowerCase()] = customClass.camelCase ? c : _utils.toDash(c);
152          });
153
154          $input        = $('<input/>', { 'class': _this.classes.input, 'readonly': isMobile });
155          $items        = $('<div/>',   { 'class': _this.classes.items, 'tabindex': -1 });
156          $itemsScroll  = $('<div/>',   { 'class': _this.classes.scroll });
157          $wrapper      = $('<div/>',   { 'class': customClass.prefix, 'html': _this.options.arrowButtonMarkup });
158          $label        = $('<p class="label"/>');
159          $outerWrapper = $original.wrap('<div>').parent().append($wrapper.prepend($label), $items, $input);
160
161          eventTriggers = {
162            open    : _open,
163            close   : _close,
164            destroy : _destroy,
165            refresh : _refresh,
166            init    : _init
167          };
168
169          $original.on(eventTriggers).wrap('<div class="' + _this.classes.hideselect + '">');
170          $.extend(_this, eventTriggers);
171
172          labelBuilder = _this.options.labelBuilder;
173
174          if ( _this.options.inheritOriginalWidth && originalWidth > 0 )
175            $outerWrapper.width(originalWidth);
176
177          _populate();
178        }
179
180        // Generate options markup and event binds
181        function _populate() {
182          _this.items = [];
183
184          var $options = $original.children(),
185              _$li = '<ul>',
186              $justOptions = $original.find('option'),
187              selectedIndex = $justOptions.index($justOptions.filter(':selected')),
188              currIndex = 0;
189
190          currValue = (selected = ~selectedIndex ? selectedIndex : 0);
191
192          if ( optionsLength = $options.length ) {
193            // Build options markup
194            $options.each(function() {
195              var $elm = $(this);
196
197              if ( $elm.is('optgroup') ) {
198                var groupDisabled = $elm.prop('disabled'),
199                    $children = $elm.children();
200
201                _$li += _utils.format('<ul class="{1}"><li class="{2}">{3}</li>',
202                  $.trim([_this.classes.group, groupDisabled ? 'disabled' : '', $elm.prop('class')].join(' ')),
203                  _this.classes.grouplabel,
204                  $elm.prop('label')
205                );
206
207                if ( groupDisabled ) {
208                  $children.prop('disabled', true);
209                }
210
211                $children.each(buildOption);
212
213                _$li += '</ul>';
214              } else {
215                buildOption.call($elm);
216              }
217
218              function buildOption() {
219                var $elm = $(this),
220                    optionText = $elm.html(),
221                    selectDisabled = $elm.prop('disabled'),
222                    itemBuilder = _this.options.optionsItemBuilder;
223
224                _this.items[currIndex] = {
225                  element  : $elm,
226                  value    : $elm.val(),
227                  text     : optionText,
228                  slug     : _utils.replaceDiacritics(optionText),
229                  disabled : selectDisabled
230                };
231
232                _$li += _utils.format('<li data-index="{1}" class="{2}">{3}</li>',
233                  currIndex,
234                  $.trim([currIndex == currValue ? 'selected' : '', currIndex == optionsLength - 1 ? 'last' : '', selectDisabled ? 'disabled' : ''].join(' ')),
235                  $.isFunction(itemBuilder) ? itemBuilder(_this.items[currIndex], $elm, currIndex) : _utils.format(itemBuilder, _this.items[currIndex])
236                );
237
238                currIndex++;
239              }
240            });
241
242            $items.append( $itemsScroll.html(_$li + '</ul>') );
243
244            $label.html(
245              $.isFunction(labelBuilder) ? labelBuilder(_this.items[currValue]) : _utils.format(labelBuilder, _this.items[currValue])
246            )
247          }
248
249          $wrapper.add($original).add($outerWrapper).add($input).off(bindSufix);
250
251          $outerWrapper.prop('class', [
252            _this.classes.wrapper,
253            $original.prop('class').replace(/\S+/g, _this.options.customClass.prefix + '-$&'),
254            _this.options.responsive ? _this.classes.responsive : ''
255          ].join(' '));
256
257          if ( !$original.prop('disabled') ) {
258            isEnabled = true;
259
260            // Not disabled, so... Removing disabled class and bind hover
261            $outerWrapper.removeClass(_this.classes.disabled).on('mouseenter' + bindSufix + ' mouseleave' + bindSufix, function(e) {
262              $(this).toggleClass(_this.classes.hover);
263
264              // Delay close effect when openOnHover is true
265              if ( _this.options.openOnHover ) {
266                clearTimeout(_this.closeTimer);
267                e.type == 'mouseleave' ? _this.closeTimer = setTimeout(_close, _this.options.hoverIntentTimeout) : _open();
268              }
269            });
270
271            // Toggle open/close
272            $wrapper.on('click' + bindSufix, function(e) {
273              isOpen ? _close() : _open(e);
274            });
275
276            $input
277              .prop({
278                tabindex: tabindex,
279                disabled: false
280              })
281              .on('keypress' + bindSufix, _handleSystemKeys)
282              .on('keydown' + bindSufix, function(e) {
283                _handleSystemKeys(e);
284
285                // Clear search
286                clearTimeout(_this.resetStr);
287                _this.resetStr = setTimeout(function() {
288                  $input.val('');
289                }, _this.options.keySearchTimeout);
290
291                var key = e.keyCode || e.which;
292
293                // If it's a directional key
294                // 37 => Left
295                // 38 => Up
296                // 39 => Right
297                // 40 => Down
298                if ( key > 36 && key < 41 ) {
299                  if ( !_this.options.allowWrap ) {
300                    if ( (key < 39 && selected == 0) || (key > 38 && (selected + 1) == _this.items.length) ) {
301                      return;
302                    }
303                  }
304
305                  _select(_utils[(key < 39 ? 'previous' : 'next') + 'EnabledItem'](_this.items, selected));
306                }
307              })
308              .on('focusin' + bindSufix, function(e) {
309                isOpen || _open(e);
310              })
311              .on('oninput' in $input[0] ? 'input' : 'keyup', function() {
312                if ( $input.val().length ) {
313                  // Search in select options
314                  $.each(_this.items, function(i, elm) {
315                    if ( RegExp('^' + $input.val(), 'i').test(elm.slug) && !elm.disabled ) {
316                      _select(i);
317                      return false;
318                    }
319                  });
320                }
321              });
322
323            $original.prop('tabindex', false);
324
325            // Remove styles from items box
326            // Fix incorrect height when refreshed is triggered with fewer options
327            $li = $('li', $items.removeAttr('style')).on({
328              // Prevent <input> blur on Chrome
329              mousedown: function(e) {
330                e.preventDefault();
331                e.stopPropagation();
332              },
333              click: function() {
334                // The second parameter is to close the box after click
335                _select($(this).data('index'), true);
336
337                // Chrome doesn't close options box if select is wrapped with a label
338                // We need to 'return false' to avoid that
339                return false;
340              }
341            }).filter('[data-index]');
342          } else {
343            $outerWrapper.addClass(_this.classes.disabled);
344            $input.prop('disabled', true);
345          }
346
347          _utils.triggerCallback('Init', _this);
348        }
349
350        function _refresh() {
351          _utils.triggerCallback('Refresh', _this);
352          _populate();
353        }
354
355        // Behavior when system keys is pressed
356        function _handleSystemKeys(e) {
357          var key = e.keyCode || e.which;
358
359          if ( key == 13 ) {
360            e.preventDefault();
361          }
362
363          // Tab / Enter / ESC
364          if ( /^(9|13|27)$/.test(key) ) {
365            e.stopPropagation();
366            _select(selected, true);
367          }
368        }
369
370        // Set options box width/height
371        function _calculateOptionsDimensions() {
372          // Calculate options box height
373          // Set a temporary class on the hidden parent of the element
374          var hiddenChildren = $items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow),
375              maxHeight = _this.options.maxHeight,
376              itemsWidth = $items.outerWidth(),
377              wrapperWidth = $wrapper.outerWidth() - (itemsWidth - $items.width());
378
379          // Set the dimensions, minimum is wrapper width, expand for long items if option is true
380          if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth )
381            finalWidth = wrapperWidth;
382          else {
383            // Make sure the scrollbar width is included
384            $items.css('overflow', 'scroll');
385
386            // Set a really long width for $outerWrapper
387            $outerWrapper.width(9e4);
388            finalWidth = $items.width();
389            // Set scroll bar to auto
390            $items.css('overflow', '');
391            $outerWrapper.width('');
392          }
393
394          $items.width(finalWidth).height() > maxHeight && $items.height(maxHeight);
395
396          // Remove the temporary class
397          hiddenChildren.removeClass(_this.classes.tempshow);
398        }
399
400        // Open the select options box
401        function _open(e) {
402          _utils.triggerCallback('BeforeOpen', _this);
403
404          if ( e ) {
405            e.preventDefault();
406            e.stopPropagation();
407          }
408
409          if ( isEnabled ) {
410            _calculateOptionsDimensions();
411
412            // Find any other opened instances of select and close it
413            $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close');
414
415            isOpen = true;
416            itemsHeight = $items.outerHeight();
417            itemsInnerHeight = $items.height();
418
419            // Toggle options box visibility
420            $outerWrapper.addClass(_this.classes.open);
421
422            // Give dummy input focus
423            $input.val('');
424            e && e.type !== 'focusin' && $input.focus();
425
426            $doc.on('click' + bindSufix, _close).on('scroll' + bindSufix, _isInViewport);
427            _isInViewport();
428
429            // Prevent window scroll when using mouse wheel inside items box
430            if ( _this.options.preventWindowScroll ) {
431              $doc.on('mousewheel' + bindSufix + ' DOMMouseScroll' + bindSufix, '.' + _this.classes.scroll, function(e) {
432                var orgEvent = e.originalEvent,
433                    scrollTop = $(this).scrollTop(),
434                    deltaY = 0;
435
436                if ( 'detail'      in orgEvent ) { deltaY = orgEvent.detail * -1; }
437                if ( 'wheelDelta'  in orgEvent ) { deltaY = orgEvent.wheelDelta;  }
438                if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; }
439                if ( 'deltaY'      in orgEvent ) { deltaY = orgEvent.deltaY * -1; }
440
441                if ( scrollTop == (this.scrollHeight - itemsInnerHeight) && deltaY < 0 || scrollTop == 0 && deltaY > 0 ) {
442                  e.preventDefault();
443                }
444              });
445            }
446
447            _detectItemVisibility(selected);
448
449            _utils.triggerCallback('Open', _this);
450          }
451        }
452
453        // Detect is the options box is inside the window
454        function _isInViewport() {
455          var scrollTop = $win.scrollTop();
456          var winHeight = $win.height();
457          var uiPosX = $outerWrapper.offset().top;
458          var uiHeight = $outerWrapper.outerHeight();
459
460          var fitsDown = (uiPosX + uiHeight + itemsHeight) <= (scrollTop + winHeight);
461          var fitsAbove = (uiPosX - itemsHeight) > scrollTop;
462
463          // If it does not fit below, only render it
464          // above it fit's there.
465          // It's acceptable that the user needs to
466          // scroll the viewport to see the cut off UI
467          var renderAbove = !fitsDown && fitsAbove;
468
469          $outerWrapper.toggleClass(_this.classes.above, renderAbove);
470        }
471
472        // Close the select options box
473        function _close() {
474          _utils.triggerCallback('BeforeClose', _this);
475
476          if ( currValue != selected ) {
477            _utils.triggerCallback('BeforeChange', _this);
478
479            var text = _this.items[selected].text;
480
481            // Apply changed value to original select
482            $original
483              .prop('selectedIndex', currValue = selected)
484              .data('value', text);
485
486            // Change label text
487            $label.html(
488              $.isFunction(labelBuilder) ? labelBuilder(_this.items[selected]) : _utils.format(labelBuilder, _this.items[selected])
489            )
490
491            _utils.triggerCallback('Change', _this);
492          }
493
494          // Remove custom events on document
495          $doc.off(bindSufix);
496
497          // Remove visible class to hide options box
498          $outerWrapper.removeClass(_this.classes.open);
499
500          isOpen = false;
501
502          _utils.triggerCallback('Close', _this);
503        }
504
505        // Select option
506        function _select(index, close) {
507          // Parameter index is required
508          if ( index == undefined ) {
509            return;
510          }
511
512          // If element is disabled, can't select it
513          if ( !_this.items[index].disabled ) {
514            // If 'close' is false (default), the options box won't close after
515            // each selected item, this is necessary for keyboard navigation
516            $li
517              .removeClass('selected')
518              .eq(selected = index)
519              .addClass('selected');
520
521            _detectItemVisibility(index);
522            close && _close();
523          }
524        }
525
526        // Detect if currently selected option is visible and scroll the options box to show it
527        function _detectItemVisibility(index) {
528          var liHeight = $li.eq(index).outerHeight(),
529              liTop = $li[index].offsetTop,
530              itemsScrollTop = $itemsScroll.scrollTop(),
531              scrollT = liTop + liHeight * 2;
532
533          $itemsScroll.scrollTop(
534            scrollT > itemsScrollTop + itemsHeight ? scrollT - itemsHeight :
535              liTop - liHeight < itemsScrollTop ? liTop - liHeight :
536                itemsScrollTop
537          );
538        }
539
540        // Unbind and remove
541        function _destroy(preserveData) {
542          if ( isEnabled ) {
543            $items.add($wrapper).add($input).remove();
544            !preserveData && $original.removeData(pluginName).removeData('value');
545            $original.prop('tabindex', tabindex).off(bindSufix).off(eventTriggers).unwrap().unwrap();
546            isEnabled = false;
547          }
548        }
549
550        _init(opts);
551      };
552
553  // A really lightweight plugin wrapper around the constructor,
554  // preventing against multiple instantiations
555  $.fn[pluginName] = function(args) {
556    return this.each(function() {
557      var data = $.data(this, pluginName);
558
559      if ( data && !data.disableOnMobile )
560        (''+args === args && data[args]) ? data[args]() : data.init(args);
561      else
562        $.data(this, pluginName, new Selectric(this, args));
563    });
564  };
565
566  $.fn[pluginName].hooks = hooks;
567
568}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.