https://www.boell-nrw.de/modules/contrib/improved_multi_select/js/improved_multi_select.js?v=10.6.15
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.