PageSourceSearch

https://www.boell-nrw.de/modules/contrib/improved_multi_select/js/improved_multi_select.js?v=10.6.15

js boell-nrw.de collected 2026-10-02 18:33:15 UTC 22,698 bytes, 567 lines download raw bytes

1/**
2 * @file
3 * Improved multi select module JS library.
4 */
5
6/**
7 * Attaches improved multi select.
8 */
9
10(function ($, Drupal, once) {
11  'use strict';
12
13  Drupal.behaviors.improved_multi_select = {
14
15    /**
16     * Detaches registered behaviors from a page element.
17     *
18     * @param {HTMLDocument|HTMLElement} [context=document]
19     *   An element to detach behaviors from.
20     * @param {object} [settings=drupalSettings]
21     *   An object containing settings for the current context.
22     * @param {string} [trigger='unload']
23     *   A string containing what's causing the behaviors to be detached.
24     */
25    detach: function (context, settings, trigger) {
26      const options = settings.improved_multi_select;
27
28      // Unbind listeners.
29      remoteListeners(context);
30
31      // Remove added elements.
32      options.selectors.forEach(function (selector) {
33        $(once('improvedselect', selector, context)).each(function () {
34          let $select = $(this);
35          let imsSelectId = $select.attr('id');
36
37          // Prevent duplicates on AJAX.
38          $('#improvedselect-' + imsSelectId).remove();
39          $('#' + imsSelectId + '-cloned').remove();
40        });
41      });
42    },
43
44    /**
45     * Attach improved multi select.
46     *
47     * @param {Element} context
48     *   The context for attaching the behavior.
49     * @param {object} settings
50     *   Options object.
51     */
52    attach: function (context, settings) {
53
54      // Add placeholder text in the search box
55      $(document).ready(function () {
56        let searchBox = $('input.improvedselect_filter');
57        if (searchBox && settings.improved_multi_select && settings.improved_multi_select.placeholder_text) {
58          searchBox.attr("placeholder", settings.improved_multi_select.placeholder_text);
59        }
60      });
61
62      if (!settings.improved_multi_select || !settings.improved_multi_select.selectors) {
63        // Let other scripts know improvedSelect has been attached.
64        $.event.trigger('improvedMultiSelectAttached');
65
66        return;
67      }
68
69      const options = settings.improved_multi_select;
70
71      options.selectors.forEach(function (selector) {
72        improvedselectAttach(selector, options, context);
73      });
74
75      // It looks like the paragraphs module leads to multiple attach calls.
76      // 1st one for an element added via AJAX, 2nd one for whole form.
77      remoteListeners(context);
78      $('.improvedselect_filter', context).bind('input', function () {
79        improvedselectFilter($(this).attr('sid'), options, context);
80      });
81
82      // Add selected items.
83      $('.improvedselect .add', context).click(function () {
84        let sid = $(this).attr('sid');
85        $('#improvedselect-' + sid + ' .improvedselect_all .selected', context).each(function () {
86          let $opt = $(this);
87          $opt.removeClass('selected');
88          improvedselectUpdateGroupVisibility($opt, 1);
89          $('#improvedselect-' + sid + ' .improvedselect_sel', context).append($opt);
90        });
91        improvedselectUpdate(sid, context);
92      });
93
94      // Remove selected items.
95      $('.improvedselect .del', context).click(function () {
96        let sid = $(this).attr('sid');
97        $('#improvedselect-' + sid + ' .improvedselect_sel .selected', context).each(function () {
98          let $opt = $(this);
99          $opt.removeClass('selected');
100          $('#improvedselect-' + sid + ' .improvedselect_all', context).append($opt);
101          improvedselectUpdateGroupVisibility($opt, 0);
102        });
103        // Force re-filtering.
104        $('#improvedselect-' + sid + ' .improvedselect_filter', context).attr('prev', '');
105        // Re-filtering will handle the rest.
106        improvedselectFilter(sid, options, context);
107        improvedselectUpdate(sid, context);
108      });
109
110      // Add all items.
111      $('.improvedselect .add_all', context).click(function () {
112        let sid = $(this).attr('sid');
113        $('#improvedselect-' + sid + ' .improvedselect_all li[isgroup!=isgroup]', context).each(function () {
114          let $opt = $(this);
115          if ($opt.css('display') !== 'none') {
116            $opt.removeClass('selected');
117            improvedselectUpdateGroupVisibility($opt, 1);
118            $('#improvedselect-' + sid + ' .improvedselect_sel', context).append($opt);
119          }
120        });
121        improvedselectUpdate(sid, context);
122      });
123
124      // Remove all items.
125      $('.improvedselect .del_all', context).click(function () {
126        let sid = $(this).attr('sid');
127        $('#improvedselect-' + sid + ' .improvedselect_sel li', context).each(function () {
128          let $opt = $(this);
129          $opt.removeClass('selected');
130          $('#improvedselect-' + sid + ' .improvedselect_all', context).append($opt);
131          improvedselectUpdateGroupVisibility($opt, 0);
132        });
133        // Force re-filtering.
134        $('#improvedselect-' + sid + ' .improvedselect_filter', 
134context).attr('prev', '');
135        // Re-filtering will handle the rest.
136        improvedselectFilter(sid, options, context);
137        improvedselectUpdate(sid, context);
138      });
139
140      // Move selected items up.
141      $('.improvedselect .move_up', context).click(function () {
142        let sid = $(this).attr('sid');
143        $('#improvedselect-' + sid + ' .improvedselect_sel .selected', context).each(function () {
144          let $selected = $(this);
145          // Don't move selected items past other selected items or there will
146          // be problems when moving multiple items at once.
147          $selected.prev(':not(.selected)').before($selected);
148        });
149        improvedselectUpdate(sid, context);
150      });
151
152      // Move selected items down.
153      $('.improvedselect .move_down', context).click(function () {
154        let sid = $(this).attr('sid');
155        // Run through the selections in reverse, otherwise problems occur
156        // when moving multiple items at once.
157        $($('#improvedselect-' + sid + ' .improvedselect_sel .selected', context).get().reverse()).each(function () {
158          let $selected = $(this);
159          // Don't move selected items past other selected items or there will
160          // be problems when moving multiple items at once.
161          $selected.next(':not(.selected)').after($selected);
162        });
163        improvedselectUpdate(sid, context);
164      });
165      // Let other scripts know improvedSelect was initialized.
166      $.event.trigger('improvedMultiSelectInitialized', [$(this)]);
167
168      // Let other scripts know improvedSelect has been attached.
169      $.event.trigger('improvedMultiSelectAttached');
170    }
171  };
172
173  /**
174   * Helper to remove listeners.
175   *
176   * @param {HTMLDocument|HTMLElement} [context=document]
177   *   An element to detach behaviors from.
178   */
179  function remoteListeners(context) {
180    $('.improvedselect_filter', context).off('input');
181    $('.improvedselect .add', context).off('click');
182    $('.improvedselect .del', context).off('click');
183    $('.improvedselect .add_all', context).off('click');
184    $('.improvedselect .del_all', context).off('click');
185    $('.improvedselect .move_up', context).off('click');
186    $('.improvedselect .move_down', context).off('click');
187  }
188
189  /**
190   * Filter the all options list.
191   *
192   * @param {String} sid
193   *   Select Id.
194   * @param {object} options
195   *   Options object.
196   * @param {Element} context
197   *   The context for attaching the behavior.
198   */
199  function improvedselectFilter(sid, options, context) {
200    let $filter = $('#improvedselect-' + sid + ' .improvedselect_filter', context);
201    // Get current selected group.
202    let $selectedGroup = $('#improvedselect-' + sid + ' .improvedselect_tabs li.selected:not(.all) a', context);
203    let text = $filter.val();
204    let pattern;
205    let regex;
206    let words;
207
208    if (text.length && text !== $filter.attr('prev')) {
209      $filter.attr('prev', text);
210
211      // Do not allow JavaScript regular expressions.
212      if (!options.js_regex) {
213        text = text.replace(/[[\]^$.\\()|?*+]/g, "\\$&").replace(/{([0-9]+,?[0-9]*)}/g, '\\{$1\\}');
214      }
215
216      switch (options.filtertype) {
217        case 'partial':
218        default:
219          pattern = text;
220          break;
221
222        case 'exact':
223          pattern = '^' + text + '$';
224          break;
225
226        case 'anywords':
227          words = text.split(' ');
228          pattern = '';
229          for (let i = 0; i < words.length; i++) {
230            if (words[i]) {
231              pattern += (pattern) ? '|\\b' + words[i] + '\\b' : '\\b' + words[i] + '\\b';
232            }
233          }
234          break;
235
236        case 'anywords_partial':
237          words = text.split(' ');
238          pattern = '';
239          for (let i = 0; i < words.length; i++) {
240            if (words[i]) {
241              pattern += (pattern) ? '|' + words[i] + '' : words[i];
242            }
243          }
244          break;
245
246        case 'allwords':
247          words = text.split(' ');
248          pattern = '^';
249          // Add a lookahead for each individual word.
250          // Lookahead is used because the words can match in any order
251          // so this makes it simpler and faster.
252          for (let i = 0; i < words.length; i++) {
253            if (words[i]) {
254              pattern += '(?=.*?\\b' + words[i] + '\\b)';
255            }
256          }
257          break;
258
259        case 'allwords_partial':
260          words = text.split(' ');
261          pattern = '^';
262          // Add a lookahead for each individual word.
263          // Lookahead is used because the words can match in any order
264          // so this makes it simpler and faster.
265          for (let i = 0; i < words.length; i++) {
266            if (words[i]) {
267              pattern += '(?=.*?' + words[i] + ')';
268            }
269          }
270          break;
271      }
272
273      // Hide JS error if a regular expression isn't completed yet.
274      try {
275        regex = new RegExp(pattern, 'i');
276      }
277      catch (e) {
278        return;
279      }
280
281      $('#improvedselect-' + sid + ' .improvedselect_all li', context).each(function () {
282        let $opt = $(this);
283        if ($opt.attr('isgroup') !== 'isgroup') {
284          let str = $opt.text();
285          if (str.match(regex) && (!$selectedGroup.length || $selectedGroup.text() === $opt.attr('group'))) {
286            $opt.show();
287            if ($opt.attr('group')) {
288              // If a group is selected we don't need to show groups.
289              if (!$selectedGroup.length) {
290                $opt.siblings('li[isgroup="isgroup"][so="---' + $opt.attr('group') + '---"]').show();
291              }
292              else {
293                $opt.siblings('li[isgroup="isgroup"][so="---' + $opt.attr('group') + '---"]').hide();
294              }
295            }
296          }
297          else {
298            $opt.hide();
299            if ($opt.attr('group')) {
300              if ($opt.siblings('li[isgroup!="isgroup"][group="' + $opt.attr('group') + '"]:visible').length === 0) {
301                $opt.siblings('li[isgroup="isgroup"][so="---' + $opt.attr('group') + '---"]').hide();
302              }
303            }
304          }
305        }
306      });
307    }
308    else {
309      if (!text.length) {
310        $filter.attr('prev', '');
311      }
312      $('#improvedselect-' + sid + ' .improvedselect_all li', context).each(function () {
313        let $opt = $(this);
314        if ($opt.attr('isgroup') !== 'isgroup') {
315          if (!$selectedGroup.length || $selectedGroup.text() === $opt.attr('group')) {
316            $opt.show();
317          }
318          else {
319            $opt.hide();
320          }
321          improvedselectUpdateGroupVisibility($opt, 0);
322        }
323      });
324    }
325  }
326
327  /**
328   * Contains attach logic for a select element.
329   *
330   * @param {String} selector
331   *   jQuery selector.
332   * @param {object} options
333   *   Options object.
334   * @param {Element} context
335   *   The context for attaching the behavior.
336   */
337  function improvedselectAttach(selector, options, context) {
338    $(once('improvedselect', selector, context)).each(function () {
339      let $select = $(this);
340      let moveButtons = '';
341      let imsSelectId = $select.attr('id');
342      let $cloned_select = null;
343      let regex = new RegExp('.+-cloned$', 'i');
344
345      // Do not process cloned selects in case if options.orderable is set.
346      if (imsSelectId.match(regex)) {
347        return;
348      }
349
350      // Remove required attribute from select elements to allow form submission and validation on Drupal side.
351      if (options.remove_required_attr) {
352        $select.removeAttr('required');
353      }
354
355      if (options.orderable) {
356        // If the select is orderable then we clone the original select to keep
357        // opt groups.
358        $cloned_select = $select.clone();
359        $cloned_select.attr('id', imsSelectId + '-cloned');
360        $cloned_select.attr('name', 'cloned-' + $select.attr('name'));
361        $cloned_select.appendTo($select.parent()).hide();
362        // Move button markup to add to the widget.
363        moveButtons = '<span class="move_up" sid="' + imsSelectId + '">' + Drupal.checkPlain(options.buttontext_moveup) + '</span>' +
364            '<span class="move_down" sid="' + imsSelectId + '">' + Drupal.checkPlain(options.buttontext_movedown) + '</span>';
365      }
366
367      $select.parent().append(
368          '<div class="improvedselect" sid="' + imsSelectId + '" id="improvedselect-' + imsSelectId + '">' +
369          '<div class="improvedselect-text-wrapper">' +
370          '<input type="text" class="improvedselect_filter" sid="' + imsSelectId + '" prev="" />' +
371          '</div><ul class="improvedselect_sel"></ul><ul class="improvedselect_all"></ul><div class="improvedselect_control">' +
372          '<span class="add" sid="' + imsSelectId + '">' + Drupal.checkPlain(options.buttontext_add) + '</span>' +
373          '<span class="del" sid="' + imsSelectId + '">' + Drupal.checkPlain(options.buttontext_del) + '</span>' +
374          '<span class="add_all" sid="' + imsSelectId + '">' + Drupal.checkPlain(options.buttontext_addall) + '</span>' +
375          '<span class="del_all" sid="' + imsSelectId + '">' + Drupal.checkPlain(options.buttontext_delall) + '</span>' +
376          moveButtons +
377          '</div><div class="clear"></div></div>');
378
379      if ($select.find('optgroup').has('option').length > 0) {
380        $select.parent().find('.improvedselect').addClass('has_group');
381        // Build groups.
382        $('#improvedselect-' + imsSelectId + ' .improvedselect-text-wrapper', context)
383            .after('<div class="improvedselect_tabs-wrapper" sid="' + imsSelectId + '"><ul class="improvedselect_tabs"></ul></div>');
384        $select.find('optgroup').has('option').each(function () {
385          $('#improvedselect-' + imsSelectId + ' .improvedselect_tabs', context)
386              .append('<li><a href="">' + $(this).attr('label') + '</a></li>');
387        });
388        // Show all groups option.
389        $('#improvedselect-' + imsSelectId + ' .improvedselect_tabs', context)
390            .prepend('<li class="all"><a href="">' + Drupal.t('All') + '</a></li>');
391        // Select group.
392        $('#improvedselect-' + imsSelectId + ' .improvedselect_tabs li a', context).click(function (e) {
393          let $group = $(this);
394          let sid = $group.parent().parent().parent().attr('sid');
395          $('#improvedselect-' + imsSelectId + ' .improvedselect_tabs li.selected', context).removeClass('selected').find('a').unwrap();
396          $group.wrap('<div>').parents('li').first().addClass('selected');
397
398          // Any existing selections in the all list need to be unselected
399          // if they aren't part of the newly selected group.
400          if (!$group.hasClass('all')) {
401            $('#improvedselect-' + imsSelectId + ' .improvedselect_all li.selected[group!="' + $group.text() + '"]', context).removeClass('selected');
402          }
403
404          // Clear the filter if we have to.
405          if (options.groupresetfilter) {
406            // Clear filter box.
407            $('#improvedselect-' + imsSelectId + ' .improvedselect_filter', 
407context).val('');
408          }
409          // Force re-filtering.
410          $('#improvedselect-' + imsSelectId + ' .improvedselect_filter', context).attr('prev', '');
411          // Re-filtering will handle the rest.
412          improvedselectFilter(sid, options, context);
413          e.preventDefault();
414        });
415        // Select all to begin.
416        $('#improvedselect-' + imsSelectId + ' .improvedselect_tabs li.all a', context).click();
417      }
418
419      $select.find('option, optgroup').each(function () {
420        let $opt = $(this);
421        let group = '';
422        if ($opt[0].tagName === 'OPTGROUP') {
423          if ($opt.has('option').length) {
424            $('#improvedselect-' + imsSelectId + ' .improvedselect_all', context)
425                .append('<li isgroup="isgroup" so="---' + $opt.attr('label') + '---">--- ' + $opt.attr('label') + ' ---</li>');
426          }
427        }
428        else {
429          group = $opt.parent('optgroup').attr('label');
430          if (group) {
431            group = ' group="' + group + '"';
432          }
433          else {
434            group = '';
435          }
436          if ($opt.attr('value') !== '_none') {
437            if ($opt.attr('selected')) {
438              $('#improvedselect-' + imsSelectId + ' .improvedselect_sel', context)
439                  .append('<li so="' + $opt.attr('value') + '"' + group + '>' + $opt.html() + '</li>');
440            }
441            else {
442              $('#improvedselect-' + imsSelectId + ' .improvedselect_all', context)
443                  .append('<li so="' + $opt.attr('value') + '"' + group + '>' + $opt.html() + '</li>');
444            }
445          }
446        }
447      });
448
449      $('#improvedselect-' + imsSelectId + ' .improvedselect_sel li, #improvedselect-' + imsSelectId + ' .improvedselect_all li[isgroup!="isgroup"]', context).click(function () {
450        $(this).toggleClass('selected');
451      });
452
453      $select.hide();
454
455      // Double click feature request.
456      $('#improvedselect-' + imsSelectId + ' .improvedselect_sel li, #improvedselect-' + imsSelectId + ' .improvedselect_all li[isgroup!="isgroup"]', context).dblclick(function () {
457        // Store selected items.
458        let selected = $(this).parent().find('li.selected');
459        let current_class = $(this).parent().attr('class');
460        // Add item.
461        if (current_class === 'improvedselect_all') {
462          $(this).parent().find('li.selected').removeClass('selected');
463          $(this).addClass('selected');
464          $(this).parent().parent().find('.add').click();
465        }
466        // Remove item.
467        else {
468          $(this).parent().find('li.selected').removeClass('selected');
469          $(this).addClass('selected');
470          $(this).parent().parent().find('.del').click();
471        }
472        // Restore selected items.
473        if (selected.length) {
474          for (let k = 0; k < selected.length; k++) {
475            if ($(selected[k]).parent().attr('class') === current_class) {
476              $(selected[k]).addClass('selected');
477            }
478          }
479        }
480      });
481
482      // Set the height of the select fields based on the height of the
483      // parent, otherwise it can end up with a lot of wasted space.
484      $('.improvedselect_sel, .improvedselect_all').each(function () {
485        if ($(this).parent().height() > 0) {
486          $(this).height($(this).parent().height() - 35);
487        }
488        // @todo: Element is hidden - we can't detect its height.
489      });
490    });
491  }
492
493  /**
494   * Update the visibility of an option's group.
495   *
496   * @param {jQuery} $opt
497   *   A jQuery object of a select option.
498   * @param {Number} numItems
499   *   How many items should be considered an empty group. Generally zero or one
500   *   depending on if an item has been or is going to be removed or added.
501   */
502  function improvedselectUpdateGroupVisibility($opt, numItems) {
503    let $selectedGroup = $opt.parents('.improvedselect').first().find('.improvedselect_tabs li.selected:not(.all) a');
504
505    // Don't show groups if a group is selected.
506    if ($opt.parent().children('li[isgroup!="isgroup"][group="' + $opt.attr('group') + '"]:visible').length <= numItems || $selectedGroup.length) {
507      $opt.siblings('li[isgroup="isgroup"][so="---' + $opt.attr('group') + '---"]').hide();
508    }
509    else {
510      $opt.siblings('li[isgroup="isgroup"][so="---' + $opt.attr('group') + '---"]').show();
511    }
512  }
513
514  /**
515   * Updates the select values by its id.
516   *
517   * @param {string} sid
518   *   Select Id.
519   * @param {Element} context
520   *   The context for attaching the behavior.
521   */
522  function improvedselectUpdate(sid, context) {
523    // If we have sorting enabled, make sure we have the results sorted.
524    let $select = $('#' + sid);
525    let $cloned_select = $('#' + sid + '-cloned');
526
527    if ($cloned_select.length) {
528      $select.find('option, optgroup').remove();
529      $cloned_select.find('option:selected').prop('selected', false);
530      $('#improvedselect-' + sid + ' .improvedselect_sel li', context).each(function () {
531        let $li = $(this);
532        $select.append($('<option></option>').attr('value', $li.attr('so')).prop('selected', true).text($li.text()));
533        $('#' + sid + '-cloned [value="' + $(this).attr('so') + '"]', context).prop('selected', true);
534      });
535    }
536    else {
537      $select.find('option:selected').prop('selected', false);
538      $('#improvedselect-' + sid + ' .improvedselect_sel li', context).each(function () {
539        $('#' + sid + ' [value="' + $(this).attr('so') + '"]', context).prop('selected', true);
540      });
541    }
542
543    $select.find('option, optgroup').each(function () {
544      let $opt = $(this);
545      if ($opt[0].tagName === 'OPTGROUP') {
546        if ($opt.has('option').length) {
547          $('#improvedselect-' + sid + ' .improvedselect_all', context).append($('#improvedselect-' + sid + ' .improvedselect_all [so="---' + $opt.attr('label') + '---"]', context));
548        }
549      }
550      else {
551        // When using reordering, the options will be from the cloned select,
552        // meaning that there will be none selected, which means that items
553        // in the selected list will not be reordered, which is what we want.
554        if ($opt.attr('selected')) {
555          $('#improvedselect-' + sid + ' .improvedselect_sel', context).append($('#improvedselect-' + sid + ' .improvedselect_sel [so="' + $opt.attr('value') + '"]', context));
556        }
557        else {
558          $('#improvedselect-' + sid + ' .improvedselect_all', context).append($('#improvedselect-' + sid + ' .improvedselect_all [so="' + $opt.attr('value') + '"]', context));
559        }
560      }
561    });
562    // Don't use the $select variable here as it might be the clone.
563    // Tell the ajax system the select has changed.
564    $('#' + sid, context).trigger('change');
565  }
566
567})(jQuery, Drupal, once);

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.