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