Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / views_ui / js / views-admin.es6.js
1 /**
2  * @file
3  * Some basic behaviors and utility functions for Views UI.
4  */
5
6 (function ($, Drupal, drupalSettings) {
7   /**
8    * @namespace
9    */
10   Drupal.viewsUi = {};
11
12   /**
13    * Improve the user experience of the views edit interface.
14    *
15    * @type {Drupal~behavior}
16    *
17    * @prop {Drupal~behaviorAttach} attach
18    *   Attaches toggling of SQL rewrite warning on the corresponding checkbox.
19    */
20   Drupal.behaviors.viewsUiEditView = {
21     attach() {
22       // Only show the SQL rewrite warning when the user has chosen the
23       // corresponding checkbox.
24       $('[data-drupal-selector="edit-query-options-disable-sql-rewrite"]').on('click', () => {
25         $('.sql-rewrite-warning').toggleClass('js-hide');
26       });
27     },
28   };
29
30   /**
31    * In the add view wizard, use the view name to prepopulate form fields such
32    * as page title and menu link.
33    *
34    * @type {Drupal~behavior}
35    *
36    * @prop {Drupal~behaviorAttach} attach
37    *   Attaches behavior for prepopulating page title and menu links, based on
38    *   view name.
39    */
40   Drupal.behaviors.viewsUiAddView = {
41     attach(context) {
42       const $context = $(context);
43       // Set up regular expressions to allow only numbers, letters, and dashes.
44       const exclude = new RegExp('[^a-z0-9\\-]+', 'g');
45       const replace = '-';
46       let suffix;
47
48       // The page title, block title, and menu link fields can all be
49       // prepopulated with the view name - no regular expression needed.
50       const $fields = $context.find('[id^="edit-page-title"], [id^="edit-block-title"], [id^="edit-page-link-properties-title"]');
51       if ($fields.length) {
52         if (!this.fieldsFiller) {
53           this.fieldsFiller = new Drupal.viewsUi.FormFieldFiller($fields);
54         }
55         else {
56           // After an AJAX response, this.fieldsFiller will still have event
57           // handlers bound to the old version of the form fields (which don't
58           // exist anymore). The event handlers need to be unbound and then
59           // rebound to the new markup. Note that jQuery.live is difficult to
60           // make work in this case because the IDs of the form fields change
61           // on every AJAX response.
62           this.fieldsFiller.rebind($fields);
63         }
64       }
65
66       // Prepopulate the path field with a URLified version of the view name.
67       const $pathField = $context.find('[id^="edit-page-path"]');
68       if ($pathField.length) {
69         if (!this.pathFiller) {
70           this.pathFiller = new Drupal.viewsUi.FormFieldFiller($pathField, exclude, replace);
71         }
72         else {
73           this.pathFiller.rebind($pathField);
74         }
75       }
76
77       // Populate the RSS feed field with a URLified version of the view name,
78       // and an .xml suffix (to make it unique).
79       const $feedField = $context.find('[id^="edit-page-feed-properties-path"]');
80       if ($feedField.length) {
81         if (!this.feedFiller) {
82           suffix = '.xml';
83           this.feedFiller = new Drupal.viewsUi.FormFieldFiller($feedField, exclude, replace, suffix);
84         }
85         else {
86           this.feedFiller.rebind($feedField);
87         }
88       }
89     },
90   };
91
92   /**
93    * Constructor for the {@link Drupal.viewsUi.FormFieldFiller} object.
94    *
95    * Prepopulates a form field based on the view name.
96    *
97    * @constructor
98    *
99    * @param {jQuery} $target
100    *   A jQuery object representing the form field or fields to prepopulate.
101    * @param {bool} [exclude=false]
102    *   A regular expression representing characters to exclude from
103    *   the target field.
104    * @param {string} [replace='']
105    *   A string to use as the replacement value for disallowed characters.
106    * @param {string} [suffix='']
107    *   A suffix to append at the end of the target field content.
108    */
109   Drupal.viewsUi.FormFieldFiller = function ($target, exclude, replace, suffix) {
110     /**
111      *
112      * @type {jQuery}
113      */
114     this.source = $('#edit-label');
115
116     /**
117      *
118      * @type {jQuery}
119      */
120     this.target = $target;
121
122     /**
123      *
124      * @type {bool}
125      */
126     this.exclude = exclude || false;
127
128     /**
129      *
130      * @type {string}
131      */
132     this.replace = replace || '';
133
134     /**
135      *
136      * @type {string}
137      */
138     this.suffix = suffix || '';
139
140     // Create bound versions of this instance's object methods to use as event
141     // handlers. This will let us easily unbind those specific handlers later
142     // on. NOTE: jQuery.proxy will not work for this because it assumes we want
143     // only one bound version of an object method, whereas we need one version
144     // per object instance.
145     const self = this;
146
147     /**
148      * Populate the target form field with the altered source field value.
149      *
150      * @return {*}
151      *   The result of the _populate call, which should be undefined.
152      */
153     this.populate = function () {
154       return self._populate.call(self);
155     };
156
157     /**
158      * Stop prepopulating the form fields.
159      *
160      * @return {*}
161      *   The result of the _unbind call, which should be undefined.
162      */
163     this.unbind = function () {
164       return self._unbind.call(self);
165     };
166
167     this.bind();
168     // Object constructor; no return value.
169   };
170
171   $.extend(Drupal.viewsUi.FormFieldFiller.prototype, /** @lends Drupal.viewsUi.FormFieldFiller# */{
172
173     /**
174      * Bind the form-filling behavior.
175      */
176     bind() {
177       this.unbind();
178       // Populate the form field when the source changes.
179       this.source.on('keyup.viewsUi change.viewsUi', this.populate);
180       // Quit populating the field as soon as it gets focus.
181       this.target.on('focus.viewsUi', this.unbind);
182     },
183
184     /**
185      * Get the source form field value as altered by the passed-in parameters.
186      *
187      * @return {string}
188      *   The source form field value.
189      */
190     getTransliterated() {
191       let from = this.source.val();
192       if (this.exclude) {
193         from = from.toLowerCase().replace(this.exclude, this.replace);
194       }
195       return from;
196     },
197
198     /**
199      * Populate the target form field with the altered source field value.
200      */
201     _populate() {
202       const transliterated = this.getTransliterated();
203       const suffix = this.suffix;
204       this.target.each(function (i) {
205         // Ensure that the maxlength is not exceeded by prepopulating the field.
206         const maxlength = $(this).attr('maxlength') - suffix.length;
207         $(this).val(transliterated.substr(0, maxlength) + suffix);
208       });
209     },
210
211     /**
212      * Stop prepopulating the form fields.
213      */
214     _unbind() {
215       this.source.off('keyup.viewsUi change.viewsUi', this.populate);
216       this.target.off('focus.viewsUi', this.unbind);
217     },
218
219     /**
220      * Bind event handlers to new form fields, after they're replaced via Ajax.
221      *
222      * @param {jQuery} $fields
223      *   Fields to rebind functionality to.
224      */
225     rebind($fields) {
226       this.target = $fields;
227       this.bind();
228     },
229   });
230
231   /**
232    * Adds functionality for the add item form.
233    *
234    * @type {Drupal~behavior}
235    *
236    * @prop {Drupal~behaviorAttach} attach
237    *   Attaches the functionality in {@link Drupal.viewsUi.AddItemForm} to the
238    *   forms in question.
239    */
240   Drupal.behaviors.addItemForm = {
241     attach(context) {
242       const $context = $(context);
243       let $form = $context;
244       // The add handler form may have an id of views-ui-add-handler-form--n.
245       if (!$context.is('form[id^="views-ui-add-handler-form"]')) {
246         $form = $context.find('form[id^="views-ui-add-handler-form"]');
247       }
248       if ($form.once('views-ui-add-handler-form').length) {
249         // If we we have an unprocessed views-ui-add-handler-form, let's
250         // instantiate.
251         new Drupal.viewsUi.AddItemForm($form);
252       }
253     },
254   };
255
256   /**
257    * Constructs a new AddItemForm.
258    *
259    * @constructor
260    *
261    * @param {jQuery} $form
262    *   The form element used.
263    */
264   Drupal.viewsUi.AddItemForm = function ($form) {
265     /**
266      *
267      * @type {jQuery}
268      */
269     this.$form = $form;
270     this.$form.find('.views-filterable-options :checkbox').on('click', $.proxy(this.handleCheck, this));
271
272     /**
273      * Find the wrapper of the displayed text.
274      */
275     this.$selected_div = this.$form.find('.views-selected-options').parent();
276     this.$selected_div.hide();
277
278     /**
279      *
280      * @type {Array}
281      */
282     this.checkedItems = [];
283   };
284
285   /**
286    * Handles a checkbox check.
287    *
288    * @param {jQuery.Event} event
289    *   The event triggered.
290    */
291   Drupal.viewsUi.AddItemForm.prototype.handleCheck = function (event) {
292     const $target = $(event.target);
293     const label = $.trim($target.closest('td').next().html());
294     // Add/remove the checked item to the list.
295     if ($target.is(':checked')) {
296       this.$selected_div.show().css('display', 'block');
297       this.checkedItems.push(label);
298     }
299     else {
300       const position = $.inArray(label, this.checkedItems);
301       // Delete the item from the list and make sure that the list doesn't have
302       // undefined items left.
303       for (let i = 0; i < this.checkedItems.length; i++) {
304         if (i === position) {
305           this.checkedItems.splice(i, 1);
306           i--;
307           break;
308         }
309       }
310       // Hide it again if none item is selected.
311       if (this.checkedItems.length === 0) {
312         this.$selected_div.hide();
313       }
314     }
315     this.refreshCheckedItems();
316   };
317
318   /**
319    * Refresh the display of the checked items.
320    */
321   Drupal.viewsUi.AddItemForm.prototype.refreshCheckedItems = function () {
322     // Perhaps we should precache the text div, too.
323     this.$selected_div.find('.views-selected-options')
324       .html(this.checkedItems.join(', '))
325       .trigger('dialogContentResize');
326   };
327
328   /**
329    * The input field items that add displays must be rendered as `<input>`
330    * elements. The following behavior detaches the `<input>` elements from the
331    * DOM, wraps them in an unordered list, then appends them to the list of
332    * tabs.
333    *
334    * @type {Drupal~behavior}
335    *
336    * @prop {Drupal~behaviorAttach} attach
337    *   Fixes the input elements needed.
338    */
339   Drupal.behaviors.viewsUiRenderAddViewButton = {
340     attach(context) {
341       // Build the add display menu and pull the display input buttons into it.
342       const $menu = $(context).find('#views-display-menu-tabs').once('views-ui-render-add-view-button');
343       if (!$menu.length) {
344         return;
345       }
346
347       const $addDisplayDropdown = $(`<li class="add"><a href="#"><span class="icon add"></span>${Drupal.t('Add')}</a><ul class="action-list" style="display:none;"></ul></li>`);
348       const $displayButtons = $menu.nextAll('input.add-display').detach();
349       $displayButtons.appendTo($addDisplayDropdown.find('.action-list')).wrap('<li>')
350         .parent().eq(0).addClass('first').end().eq(-1).addClass('last');
351       // Remove the 'Add ' prefix from the button labels since they're being
352       // placed in an 'Add' dropdown. @todo This assumes English, but so does
353       // $addDisplayDropdown above. Add support for translation.
354       $displayButtons.each(function () {
355         const label = $(this).val();
356         if (label.substr(0, 4) === 'Add ') {
357           $(this).val(label.substr(4));
358         }
359       });
360       $addDisplayDropdown.appendTo($menu);
361
362       // Add the click handler for the add display button.
363       $menu.find('li.add > a').on('click', function (event) {
364         event.preventDefault();
365         const $trigger = $(this);
366         Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger);
367       });
368       // Add a mouseleave handler to close the dropdown when the user mouses
369       // away from the item. We use mouseleave instead of mouseout because
370       // the user is going to trigger mouseout when she moves from the trigger
371       // link to the sub menu items.
372       // We use the live binder because the open class on this item will be
373       // toggled on and off and we want the handler to take effect in the cases
374       // that the class is present, but not when it isn't.
375       $('li.add', $menu).on('mouseleave', function (event) {
376         const $this = $(this);
377         const $trigger = $this.children('a[href="#"]');
378         if ($this.children('.action-list').is(':visible')) {
379           Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger);
380         }
381       });
382     },
383   };
384
385   /**
386    * Toggle menu visibility.
387    *
388    * @param {jQuery} $trigger
389    *   The element where the toggle was triggered.
390    *
391    *
392    * @note [@jessebeach] I feel like the following should be a more generic
393    *   function and not written specifically for this UI, but I'm not sure
394    *   where to put it.
395    */
396   Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu = function ($trigger) {
397     $trigger.parent().toggleClass('open');
398     $trigger.next().slideToggle('fast');
399   };
400
401   /**
402    * Add search options to the views ui.
403    *
404    * @type {Drupal~behavior}
405    *
406    * @prop {Drupal~behaviorAttach} attach
407    *   Attaches {@link Drupal.viewsUi.OptionsSearch} to the views ui filter
408    *   options.
409    */
410   Drupal.behaviors.viewsUiSearchOptions = {
411     attach(context) {
412       const $context = $(context);
413       let $form = $context;
414       // The add handler form may have an id of views-ui-add-handler-form--n.
415       if (!$context.is('form[id^="views-ui-add-handler-form"]')) {
416         $form = $context.find('form[id^="views-ui-add-handler-form"]');
417       }
418       // Make sure we don't add more than one event handler to the same form.
419       if ($form.once('views-ui-filter-options').length) {
420         new Drupal.viewsUi.OptionsSearch($form);
421       }
422     },
423   };
424
425   /**
426    * Constructor for the viewsUi.OptionsSearch object.
427    *
428    * The OptionsSearch object filters the available options on a form according
429    * to the user's search term. Typing in "taxonomy" will show only those
430    * options containing "taxonomy" in their label.
431    *
432    * @constructor
433    *
434    * @param {jQuery} $form
435    *   The form element.
436    */
437   Drupal.viewsUi.OptionsSearch = function ($form) {
438     /**
439      *
440      * @type {jQuery}
441      */
442     this.$form = $form;
443
444     // Click on the title checks the box.
445     this.$form.on('click', 'td.title', (event) => {
446       const $target = $(event.currentTarget);
447       $target.closest('tr').find('input').trigger('click');
448     });
449
450     const searchBoxSelector = '[data-drupal-selector="edit-override-controls-options-search"]';
451     const controlGroupSelector = '[data-drupal-selector="edit-override-controls-group"]';
452     this.$form.on('formUpdated', `${searchBoxSelector},${controlGroupSelector}`, $.proxy(this.handleFilter, this));
453
454     this.$searchBox = this.$form.find(searchBoxSelector);
455     this.$controlGroup = this.$form.find(controlGroupSelector);
456
457     /**
458      * Get a list of option labels and their corresponding divs and maintain it
459      * in memory, so we have as little overhead as possible at keyup time.
460      */
461     this.options = this.getOptions(this.$form.find('.filterable-option'));
462
463     // Trap the ENTER key in the search box so that it doesn't submit the form.
464     this.$searchBox.on('keypress', (event) => {
465       if (event.which === 13) {
466         event.preventDefault();
467       }
468     });
469   };
470
471   $.extend(Drupal.viewsUi.OptionsSearch.prototype, /** @lends Drupal.viewsUi.OptionsSearch# */{
472
473     /**
474      * Assemble a list of all the filterable options on the form.
475      *
476      * @param {jQuery} $allOptions
477      *   A jQuery object representing the rows of filterable options to be
478      *   shown and hidden depending on the user's search terms.
479      *
480      * @return {Array}
481      *   An array of all the filterable options.
482      */
483     getOptions($allOptions) {
484       let $title;
485       let $description;
486       let $option;
487       const options = [];
488       const length = $allOptions.length;
489       for (let i = 0; i < length; i++) {
490         $option = $($allOptions[i]);
491         $title = $option.find('.title');
492         $description = $option.find('.description');
493         options[i] = {
494           // Search on the lowercase version of the title text + description.
495           searchText: `${$title.text().toLowerCase()} ${$description.text().toLowerCase()}`,
496           // Maintain a reference to the jQuery object for each row, so we don't
497           // have to create a new object inside the performance-sensitive keyup
498           // handler.
499           $div: $option,
500         };
501       }
502       return options;
503     },
504
505     /**
506      * Filter handler for the search box and type select that hides or shows the relevant
507      * options.
508      *
509      * @param {jQuery.Event} event
510      *   The formUpdated event.
511      */
512     handleFilter(event) {
513       // Determine the user's search query. The search text has been converted
514       // to lowercase.
515       const search = this.$searchBox.val().toLowerCase();
516       const words = search.split(' ');
517       // Get selected Group
518       const group = this.$controlGroup.val();
519
520       // Search through the search texts in the form for matching text.
521       this.options.forEach((option) => {
522         function hasWord(word) {
523           return option.searchText.indexOf(word) !== -1;
524         }
525
526         let found = true;
527         // Each word in the search string has to match the item in order for the
528         // item to be shown.
529         if (search) {
530           found = words.every(hasWord);
531         }
532         if (found && group !== 'all') {
533           found = option.$div.hasClass(group);
534         }
535
536         option.$div.toggle(found);
537       });
538
539       // Adapt dialog to content size.
540       $(event.target).trigger('dialogContentResize');
541     },
542   });
543
544   /**
545    * Preview functionality in the views edit form.
546    *
547    * @type {Drupal~behavior}
548    *
549    * @prop {Drupal~behaviorAttach} attach
550    *   Attaches the preview functionality to the view edit form.
551    */
552   Drupal.behaviors.viewsUiPreview = {
553     attach(context) {
554       // Only act on the edit view form.
555       const $contextualFiltersBucket = $(context).find('.views-display-column .views-ui-display-tab-bucket.argument');
556       if ($contextualFiltersBucket.length === 0) {
557         return;
558       }
559
560       // If the display has no contextual filters, hide the form where you
561       // enter the contextual filters for the live preview. If it has contextual
562       // filters, show the form.
563       const $contextualFilters = $contextualFiltersBucket.find('.views-display-setting a');
564       if ($contextualFilters.length) {
565         $('#preview-args').parent().show();
566       }
567       else {
568         $('#preview-args').parent().hide();
569       }
570
571       // Executes an initial preview.
572       if ($('#edit-displays-live-preview').once('edit-displays-live-preview').is(':checked')) {
573         $('#preview-submit').once('edit-displays-live-preview').trigger('click');
574       }
575     },
576   };
577
578   /**
579    * Rearranges the filters.
580    *
581    * @type {Drupal~behavior}
582    *
583    * @prop {Drupal~behaviorAttach} attach
584    *   Attach handlers to make it possible to rearange the filters in the form
585    *   in question.
586    *   @see Drupal.viewsUi.RearrangeFilterHandler
587    */
588   Drupal.behaviors.viewsUiRearrangeFilter = {
589     attach(context) {
590       // Only act on the rearrange filter form.
591       if (typeof Drupal.tableDrag === 'undefined' || typeof Drupal.tableDrag['views-rearrange-filters'] === 'undefined') {
592         return;
593       }
594       const $context = $(context);
595       const $table = $context.find('#views-rearrange-filters').once('views-rearrange-filters');
596       const $operator = $context.find('.js-form-item-filter-groups-operator').once('views-rearrange-filters');
597       if ($table.length) {
598         new Drupal.viewsUi.RearrangeFilterHandler($table, $operator);
599       }
600     },
601   };
602
603   /**
604    * Improve the UI of the rearrange filters dialog box.
605    *
606    * @constructor
607    *
608    * @param {jQuery} $table
609    *   The table in the filter form.
610    * @param {jQuery} $operator
611    *   The filter groups operator element.
612    */
613   Drupal.viewsUi.RearrangeFilterHandler = function ($table, $operator) {
614     /**
615      * Keep a reference to the `<table>` being altered and to the div containing
616      * the filter groups operator dropdown (if it exists).
617      */
618     this.table = $table;
619
620     /**
621      *
622      * @type {jQuery}
623      */
624     this.operator = $operator;
625
626     /**
627      *
628      * @type {bool}
629      */
630     this.hasGroupOperator = this.operator.length > 0;
631
632     /**
633      * Keep a reference to all draggable rows within the table.
634      *
635      * @type {jQuery}
636      */
637     this.draggableRows = $table.find('.draggable');
638
639     /**
640      * Keep a reference to the buttons for adding and removing filter groups.
641      *
642      * @type {jQuery}
643      */
644     this.addGroupButton = $('input#views-add-group');
645
646     /**
647      * @type {jQuery}
648      */
649     this.removeGroupButtons = $table.find('input.views-remove-group');
650
651     // Add links that duplicate the functionality of the (hidden) add and remove
652     // buttons.
653     this.insertAddRemoveFilterGroupLinks();
654
655     // When there is a filter groups operator dropdown on the page, create
656     // duplicates of the dropdown between each pair of filter groups.
657     if (this.hasGroupOperator) {
658       /**
659        * @type {jQuery}
660        */
661       this.dropdowns = this.duplicateGroupsOperator();
662       this.syncGroupsOperators();
663     }
664
665     // Add methods to the tableDrag instance to account for operator cells
666     // (which span multiple rows), the operator labels next to each filter
667     // (e.g., "And" or "Or"), the filter groups, and other special aspects of
668     // this tableDrag instance.
669     this.modifyTableDrag();
670
671     // Initialize the operator labels (e.g., "And" or "Or") that are displayed
672     // next to the filters in each group, and bind a handler so that they change
673     // based on the values of the operator dropdown within that group.
674     this.redrawOperatorLabels();
675     $table.find('.views-group-title select')
676       .once('views-rearrange-filter-handler')
677       .on('change.views-rearrange-filter-handler', $.proxy(this, 'redrawOperatorLabels'));
678
679     // Bind handlers so that when a "Remove" link is clicked, we:
680     // - Update the rowspans of cells containing an operator dropdown (since
681     //   they need to change to reflect the number of rows in each group).
682     // - Redraw the operator labels next to the filters in the group (since the
683     //   filter that is currently displayed last in each group is not supposed
684     //   to have a label display next to it).
685     $table.find('a.views-groups-remove-link')
686       .once('views-rearrange-filter-handler')
687       .on('click.views-rearrange-filter-handler', $.proxy(this, 'updateRowspans'))
688       .on('click.views-rearrange-filter-handler', $.proxy(this, 'redrawOperatorLabels'));
689   };
690
691   $.extend(Drupal.viewsUi.RearrangeFilterHandler.prototype, /** @lends Drupal.viewsUi.RearrangeFilterHandler# */{
692
693     /**
694      * Insert links that allow filter groups to be added and removed.
695      */
696     insertAddRemoveFilterGroupLinks() {
697       // Insert a link for adding a new group at the top of the page, and make
698       // it match the action link styling used in a typical page.html.twig.
699       // Since Drupal does not provide a theme function for this markup this is
700       // the best we can do.
701       $(`<ul class="action-links"><li><a id="views-add-group-link" href="#">${this.addGroupButton.val()}</a></li></ul>`)
702         .prependTo(this.table.parent())
703         // When the link is clicked, dynamically click the hidden form button
704         // for adding a new filter group.
705         .once('views-rearrange-filter-handler')
706         .find('#views-add-group-link')
707         .on('click.views-rearrange-filter-handler', $.proxy(this, 'clickAddGroupButton'));
708
709       // Find each (visually hidden) button for removing a filter group and
710       // insert a link next to it.
711       const length = this.removeGroupButtons.length;
712       let i;
713       for (i = 0; i < length; i++) {
714         const $removeGroupButton = $(this.removeGroupButtons[i]);
715         const buttonId = $removeGroupButton.attr('id');
716         $(`<a href="#" class="views-remove-group-link">${Drupal.t('Remove group')}</a>`)
717           .insertBefore($removeGroupButton)
718           // When the link is clicked, dynamically click the corresponding form
719           // button.
720           .once('views-rearrange-filter-handler')
721           .on('click.views-rearrange-filter-handler', { buttonId }, $.proxy(this, 'clickRemoveGroupButton'));
722       }
723     },
724
725     /**
726      * Dynamically click the button that adds a new filter group.
727      *
728      * @param {jQuery.Event} event
729      *   The event triggered.
730      */
731     clickAddGroupButton(event) {
732       this.addGroupButton.trigger('mousedown');
733       event.preventDefault();
734     },
735
736     /**
737      * Dynamically click a button for removing a filter group.
738      *
739      * @param {jQuery.Event} event
740      *   Event being triggered, with event.data.buttonId set to the ID of the
741      *   form button that should be clicked.
742      */
743     clickRemoveGroupButton(event) {
744       this.table.find(`#${event.data.buttonId}`).trigger('mousedown');
745       event.preventDefault();
746     },
747
748     /**
749      * Move the groups operator so that it's between the first two groups, and
750      * duplicate it between any subsequent groups.
751      *
752      * @return {jQuery}
753      *   An operator element.
754      */
755     duplicateGroupsOperator() {
756       let dropdowns;
757       let newRow;
758       let titleRow;
759
760       const titleRows = $('tr.views-group-title').once('duplicateGroupsOperator');
761
762       if (!titleRows.length) {
763         return this.operator;
764       }
765
766       // Get rid of the explanatory text around the operator; its placement is
767       // explanatory enough.
768       this.operator.find('label').add('div.description').addClass('visually-hidden');
769       this.operator.find('select').addClass('form-select');
770
771       // Keep a list of the operator dropdowns, so we can sync their behavior
772       // later.
773       dropdowns = this.operator;
774
775       // Move the operator to a new row just above the second group.
776       titleRow = $('tr#views-group-title-2');
777       newRow = $('<tr class="filter-group-operator-row"><td colspan="5"></td></tr>');
778       newRow.find('td').append(this.operator);
779       newRow.insertBefore(titleRow);
780       const length = titleRows.length;
781       // Starting with the third group, copy the operator to a new row above the
782       // group title.
783       for (let i = 2; i < length; i++) {
784         titleRow = $(titleRows[i]);
785         // Make a copy of the operator dropdown and put it in a new table row.
786         const fakeOperator = this.operator.clone();
787         fakeOperator.attr('id', '');
788         newRow = $('<tr class="filter-group-operator-row"><td colspan="5"></td></tr>');
789         newRow.find('td').append(fakeOperator);
790         newRow.insertBefore(titleRow);
791         dropdowns.add(fakeOperator);
792       }
793
794       return dropdowns;
795     },
796
797     /**
798      * Make the duplicated groups operators change in sync with each other.
799      */
800     syncGroupsOperators() {
801       if (this.dropdowns.length < 2) {
802         // We only have one dropdown (or none at all), so there's nothing to
803         // sync.
804         return;
805       }
806
807       this.dropdowns.on('change', $.proxy(this, 'operatorChangeHandler'));
808     },
809
810     /**
811      * Click handler for the operators that appear between filter groups.
812      *
813      * Forces all operator dropdowns to have the same value.
814      *
815      * @param {jQuery.Event} event
816      *   The event triggered.
817      */
818     operatorChangeHandler(event) {
819       const $target = $(event.target);
820       const operators = this.dropdowns.find('select').not($target);
821
822       // Change the other operators to match this new value.
823       operators.val($target.val());
824     },
825
826     /**
827      * @method
828      */
829     modifyTableDrag() {
830       const tableDrag = Drupal.tableDrag['views-rearrange-filters'];
831       const filterHandler = this;
832
833       /**
834        * Override the row.onSwap method from tabledrag.js.
835        *
836        * When a row is dragged to another place in the table, several things
837        * need to occur.
838        * - The row needs to be moved so that it's within one of the filter
839        * groups.
840        * - The operator cells that span multiple rows need their rowspan
841        * attributes updated to reflect the number of rows in each group.
842        * - The operator labels that are displayed next to each filter need to
843        * be redrawn, to account for the row's new location.
844        */
845       tableDrag.row.prototype.onSwap = function () {
846         if (filterHandler.hasGroupOperator) {
847           // Make sure the row that just got moved (this.group) is inside one
848           // of the filter groups (i.e. below an empty marker row or a
849           // draggable). If it isn't, move it down one.
850           const thisRow = $(this.group);
851           const previousRow = thisRow.prev('tr');
852           if (previousRow.length && !previousRow.hasClass('group-message') && !previousRow.hasClass('draggable')) {
853             // Move the dragged row down one.
854             const next = thisRow.next();
855             if (next.is('tr')) {
856               this.swap('after', next);
857             }
858           }
859           filterHandler.updateRowspans();
860         }
861         // Redraw the operator labels that are displayed next to each filter, to
862         // account for the row's new location.
863         filterHandler.redrawOperatorLabels();
864       };
865
866       /**
867        * Override the onDrop method from tabledrag.js.
868        */
869       tableDrag.onDrop = function () {
870         // If the tabledrag change marker (i.e., the "*") has been inserted
871         // inside a row after the operator label (i.e., "And" or "Or")
872         // rearrange the items so the operator label continues to appear last.
873         const changeMarker = $(this.oldRowElement).find('.tabledrag-changed');
874         if (changeMarker.length) {
875           // Search for occurrences of the operator label before the change
876           // marker, and reverse them.
877           const operatorLabel = changeMarker.prevAll('.views-operator-label');
878           if (operatorLabel.length) {
879             operatorLabel.insertAfter(changeMarker);
880           }
881         }
882
883         // Make sure the "group" dropdown is properly updated when rows are
884         // dragged into an empty filter group. This is borrowed heavily from
885         // the block.js implementation of tableDrag.onDrop().
886         const groupRow = $(this.rowObject.element).prevAll('tr.group-message').get(0);
887         const groupName = groupRow.className.replace(/([^ ]+[ ]+)*group-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
888         const groupField = $('select.views-group-select', this.rowObject.element);
889         if ($(this.rowObject.element).prev('tr').is('.group-message') && !groupField.is(`.views-group-select-${groupName}`)) {
890           const oldGroupName = groupField.attr('class').replace(/([^ ]+[ ]+)*views-group-select-([^ ]+)([ ]+[^ ]+)*/, '$2');
891           groupField.removeClass(`views-group-select-${oldGroupName}`).addClass(`views-group-select-${groupName}`);
892           groupField.val(groupName);
893         }
894       };
895     },
896
897     /**
898      * Redraw the operator labels that are displayed next to each filter.
899      */
900     redrawOperatorLabels() {
901       for (let i = 0; i < this.draggableRows.length; i++) {
902         // Within the row, the operator labels are displayed inside the first
903         // table cell (next to the filter name).
904         const $draggableRow = $(this.draggableRows[i]);
905         const $firstCell = $draggableRow.find('td').eq(0);
906         if ($firstCell.length) {
907           // The value of the operator label ("And" or "Or") is taken from the
908           // first operator dropdown we encounter, going backwards from the
909           // current row. This dropdown is the one associated with the current
910           // row's filter group.
911           const operatorValue = $draggableRow.prevAll('.views-group-title').find('option:selected').html();
912           const operatorLabel = `<span class="views-operator-label">${operatorValue}</span>`;
913           // If the next visible row after this one is a draggable filter row,
914           // display the operator label next to the current row. (Checking for
915           // visibility is necessary here since the "Remove" links hide the
916           // removed row but don't actually remove it from the document).
917           const $nextRow = $draggableRow.nextAll(':visible').eq(0);
918           const $existingOperatorLabel = $firstCell.find('.views-operator-label');
919           if ($nextRow.hasClass('draggable')) {
920             // If an operator label was already there, replace it with the new
921             // one.
922             if ($existingOperatorLabel.length) {
923               $existingOperatorLabel.replaceWith(operatorLabel);
924             }
925             // Otherwise, append the operator label to the end of the table
926             // cell.
927             else {
928               $firstCell.append(operatorLabel);
929             }
930           }
931           // If the next row doesn't contain a filter, then this is the last row
932           // in the group. We don't want to display the operator there (since
933           // operators should only display between two related filters, e.g.
934           // "filter1 AND filter2 AND filter3"). So we remove any existing label
935           // that this row has.
936           else {
937             $existingOperatorLabel.remove();
938           }
939         }
940       }
941     },
942
943     /**
944      * Update the rowspan attribute of each cell containing an operator
945      * dropdown.
946      */
947     updateRowspans() {
948       let $row;
949       let $currentEmptyRow;
950       let draggableCount;
951       let $operatorCell;
952       const rows = $(this.table).find('tr');
953       const length = rows.length;
954       for (let i = 0; i < length; i++) {
955         $row = $(rows[i]);
956         if ($row.hasClass('views-group-title')) {
957           // This row is a title row.
958           // Keep a reference to the cell containing the dropdown operator.
959           $operatorCell = $row.find('td.group-operator');
960           // Assume this filter group is empty, until we find otherwise.
961           draggableCount = 0;
962           $currentEmptyRow = $row.next('tr');
963           $currentEmptyRow.removeClass('group-populated').addClass('group-empty');
964           // The cell with the dropdown operator should span the title row and
965           // the "this group is empty" row.
966           $operatorCell.attr('rowspan', 2);
967         }
968         else if ($row.hasClass('draggable') && $row.is(':visible')) {
969           // We've found a visible filter row, so we now know the group isn't
970           // empty.
971           draggableCount++;
972           $currentEmptyRow.removeClass('group-empty').addClass('group-populated');
973           // The operator cell should span all draggable rows, plus the title.
974           $operatorCell.attr('rowspan', draggableCount + 1);
975         }
976       }
977     },
978   });
979
980   /**
981    * Add a select all checkbox, which checks each checkbox at once.
982    *
983    * @type {Drupal~behavior}
984    *
985    * @prop {Drupal~behaviorAttach} attach
986    *   Attaches select all functionality to the views filter form.
987    */
988   Drupal.behaviors.viewsFilterConfigSelectAll = {
989     attach(context) {
990       const $context = $(context);
991
992       const $selectAll = $context.find('.js-form-item-options-value-all').once('filterConfigSelectAll');
993       const $selectAllCheckbox = $selectAll.find('input[type=checkbox]');
994       const $checkboxes = $selectAll.closest('.form-checkboxes').find('.js-form-type-checkbox:not(.js-form-item-options-value-all) input[type="checkbox"]');
995
996       if ($selectAll.length) {
997          // Show the select all checkbox.
998         $selectAll.show();
999         $selectAllCheckbox.on('click', function () {
1000           // Update all checkbox beside the select all checkbox.
1001           $checkboxes.prop('checked', $(this).is(':checked'));
1002         });
1003
1004         // Uncheck the select all checkbox if any of the others are unchecked.
1005         $checkboxes.on('click', function () {
1006           if ($(this).is('checked') === false) {
1007             $selectAllCheckbox.prop('checked', false);
1008           }
1009         });
1010       }
1011     },
1012   };
1013
1014   /**
1015    * Remove icon class from elements that are themed as buttons or dropbuttons.
1016    *
1017    * @type {Drupal~behavior}
1018    *
1019    * @prop {Drupal~behaviorAttach} attach
1020    *   Removes the icon class from certain views elements.
1021    */
1022   Drupal.behaviors.viewsRemoveIconClass = {
1023     attach(context) {
1024       $(context).find('.dropbutton').once('dropbutton-icon').find('.icon').removeClass('icon');
1025     },
1026   };
1027
1028   /**
1029    * Change "Expose filter" buttons into checkboxes.
1030    *
1031    * @type {Drupal~behavior}
1032    *
1033    * @prop {Drupal~behaviorAttach} attach
1034    *   Changes buttons into checkboxes via {@link Drupal.viewsUi.Checkboxifier}.
1035    */
1036   Drupal.behaviors.viewsUiCheckboxify = {
1037     attach(context, settings) {
1038       const $buttons = $('[data-drupal-selector="edit-options-expose-button-button"], [data-drupal-selector="edit-options-group-button-button"]').once('views-ui-checkboxify');
1039       const length = $buttons.length;
1040       let i;
1041       for (i = 0; i < length; i++) {
1042         new Drupal.viewsUi.Checkboxifier($buttons[i]);
1043       }
1044     },
1045   };
1046
1047   /**
1048    * Change the default widget to select the default group according to the
1049    * selected widget for the exposed group.
1050    *
1051    * @type {Drupal~behavior}
1052    *
1053    * @prop {Drupal~behaviorAttach} attach
1054    *   Changes the default widget based on user input.
1055    */
1056   Drupal.behaviors.viewsUiChangeDefaultWidget = {
1057     attach(context) {
1058       const $context = $(context);
1059
1060       function changeDefaultWidget(event) {
1061         if ($(event.target).prop('checked')) {
1062           $context.find('input.default-radios').parent().hide();
1063           $context.find('td.any-default-radios-row').parent().hide();
1064           $context.find('input.default-checkboxes').parent().show();
1065         }
1066         else {
1067           $context.find('input.default-checkboxes').parent().hide();
1068           $context.find('td.any-default-radios-row').parent().show();
1069           $context.find('input.default-radios').parent().show();
1070         }
1071       }
1072
1073       // Update on widget change.
1074       $context.find('input[name="options[group_info][multiple]"]')
1075         .on('change', changeDefaultWidget)
1076         // Update the first time the form is rendered.
1077         .trigger('change');
1078     },
1079   };
1080
1081   /**
1082    * Attaches expose filter button to a checkbox that triggers its click event.
1083    *
1084    * @constructor
1085    *
1086    * @param {HTMLElement} button
1087    *   The DOM object representing the button to be checkboxified.
1088    */
1089   Drupal.viewsUi.Checkboxifier = function (button) {
1090     this.$button = $(button);
1091     this.$parent = this.$button.parent('div.views-expose, div.views-grouped');
1092     this.$input = this.$parent.find('input:checkbox, input:radio');
1093     // Hide the button and its description.
1094     this.$button.hide();
1095     this.$parent.find('.exposed-description, .grouped-description').hide();
1096
1097     this.$input.on('click', $.proxy(this, 'clickHandler'));
1098   };
1099
1100   /**
1101    * When the checkbox is checked or unchecked, simulate a button press.
1102    *
1103    * @param {jQuery.Event} e
1104    *   The event triggered.
1105    */
1106   Drupal.viewsUi.Checkboxifier.prototype.clickHandler = function (e) {
1107     this.$button
1108       .trigger('click')
1109       .trigger('submit');
1110   };
1111
1112   /**
1113    * Change the Apply button text based upon the override select state.
1114    *
1115    * @type {Drupal~behavior}
1116    *
1117    * @prop {Drupal~behaviorAttach} attach
1118    *   Attaches behavior to change the Apply button according to the current
1119    *   state.
1120    */
1121   Drupal.behaviors.viewsUiOverrideSelect = {
1122     attach(context) {
1123       $(context).find('[data-drupal-selector="edit-override-dropdown"]').once('views-ui-override-button-text').each(function () {
1124         // Closures! :(
1125         const $context = $(context);
1126         const $submit = $context.find('[id^=edit-submit]');
1127         const old_value = $submit.val();
1128
1129         $submit.once('views-ui-override-button-text')
1130           .on('mouseup', function () {
1131             $(this).val(old_value);
1132             return true;
1133           });
1134
1135         $(this).on('change', function () {
1136           const $this = $(this);
1137           if ($this.val() === 'default') {
1138             $submit.val(Drupal.t('Apply (all displays)'));
1139           }
1140           else if ($this.val() === 'default_revert') {
1141             $submit.val(Drupal.t('Revert to default'));
1142           }
1143           else {
1144             $submit.val(Drupal.t('Apply (this display)'));
1145           }
1146           const $dialog = $context.closest('.ui-dialog-content');
1147           $dialog.trigger('dialogButtonsChange');
1148         })
1149           .trigger('change');
1150       });
1151     },
1152   };
1153
1154   /**
1155    * Functionality for the remove link in the views UI.
1156    *
1157    * @type {Drupal~behavior}
1158    *
1159    * @prop {Drupal~behaviorAttach} attach
1160    *   Attaches behavior for the remove view and remove display links.
1161    */
1162   Drupal.behaviors.viewsUiHandlerRemoveLink = {
1163     attach(context) {
1164       const $context = $(context);
1165       // Handle handler deletion by looking for the hidden checkbox and hiding
1166       // the row.
1167       $context.find('a.views-remove-link').once('views').on('click', function (event) {
1168         const id = $(this).attr('id').replace('views-remove-link-', '');
1169         $context.find(`#views-row-${id}`).hide();
1170         $context.find(`#views-removed-${id}`).prop('checked', true);
1171         event.preventDefault();
1172       });
1173
1174       // Handle display deletion by looking for the hidden checkbox and hiding
1175       // the row.
1176       $context.find('a.display-remove-link').once('display').on('click', function (event) {
1177         const id = $(this).attr('id').replace('display-remove-link-', '');
1178         $context.find(`#display-row-${id}`).hide();
1179         $context.find(`#display-removed-${id}`).prop('checked', true);
1180         event.preventDefault();
1181       });
1182     },
1183   };
1184 }(jQuery, Drupal, drupalSettings));