annotate core/modules/views_ui/js/views-admin.es6.js @ 19:fa3358dc1485 tip

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