Chris@0: /** Chris@0: * @file Chris@0: * Some basic behaviors and utility functions for Views UI. Chris@0: */ Chris@0: Chris@17: (function($, Drupal, drupalSettings) { Chris@0: /** Chris@0: * @namespace Chris@0: */ Chris@0: Drupal.viewsUi = {}; Chris@0: Chris@0: /** Chris@0: * Improve the user experience of the views edit interface. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches toggling of SQL rewrite warning on the corresponding checkbox. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiEditView = { Chris@0: attach() { Chris@0: // Only show the SQL rewrite warning when the user has chosen the Chris@0: // corresponding checkbox. Chris@17: $('[data-drupal-selector="edit-query-options-disable-sql-rewrite"]').on( Chris@17: 'click', Chris@17: () => { Chris@17: $('.sql-rewrite-warning').toggleClass('js-hide'); Chris@17: }, Chris@17: ); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * In the add view wizard, use the view name to prepopulate form fields such Chris@0: * as page title and menu link. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches behavior for prepopulating page title and menu links, based on Chris@0: * view name. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiAddView = { Chris@0: attach(context) { Chris@0: const $context = $(context); Chris@0: // Set up regular expressions to allow only numbers, letters, and dashes. Chris@0: const exclude = new RegExp('[^a-z0-9\\-]+', 'g'); Chris@0: const replace = '-'; Chris@0: let suffix; Chris@0: Chris@0: // The page title, block title, and menu link fields can all be Chris@0: // prepopulated with the view name - no regular expression needed. Chris@17: const $fields = $context.find( Chris@17: '[id^="edit-page-title"], [id^="edit-block-title"], [id^="edit-page-link-properties-title"]', Chris@17: ); Chris@0: if ($fields.length) { Chris@0: if (!this.fieldsFiller) { Chris@0: this.fieldsFiller = new Drupal.viewsUi.FormFieldFiller($fields); Chris@17: } else { Chris@0: // After an AJAX response, this.fieldsFiller will still have event Chris@0: // handlers bound to the old version of the form fields (which don't Chris@0: // exist anymore). The event handlers need to be unbound and then Chris@0: // rebound to the new markup. Note that jQuery.live is difficult to Chris@0: // make work in this case because the IDs of the form fields change Chris@0: // on every AJAX response. Chris@0: this.fieldsFiller.rebind($fields); Chris@0: } Chris@0: } Chris@0: Chris@0: // Prepopulate the path field with a URLified version of the view name. Chris@0: const $pathField = $context.find('[id^="edit-page-path"]'); Chris@0: if ($pathField.length) { Chris@0: if (!this.pathFiller) { Chris@17: this.pathFiller = new Drupal.viewsUi.FormFieldFiller( Chris@17: $pathField, Chris@17: exclude, Chris@17: replace, Chris@17: ); Chris@17: } else { Chris@0: this.pathFiller.rebind($pathField); Chris@0: } Chris@0: } Chris@0: Chris@0: // Populate the RSS feed field with a URLified version of the view name, Chris@0: // and an .xml suffix (to make it unique). Chris@17: const $feedField = $context.find( Chris@17: '[id^="edit-page-feed-properties-path"]', Chris@17: ); Chris@0: if ($feedField.length) { Chris@0: if (!this.feedFiller) { Chris@0: suffix = '.xml'; Chris@17: this.feedFiller = new Drupal.viewsUi.FormFieldFiller( Chris@17: $feedField, Chris@17: exclude, Chris@17: replace, Chris@17: suffix, Chris@17: ); Chris@17: } else { Chris@0: this.feedFiller.rebind($feedField); Chris@0: } Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Constructor for the {@link Drupal.viewsUi.FormFieldFiller} object. Chris@0: * Chris@0: * Prepopulates a form field based on the view name. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {jQuery} $target Chris@0: * A jQuery object representing the form field or fields to prepopulate. Chris@0: * @param {bool} [exclude=false] Chris@0: * A regular expression representing characters to exclude from Chris@0: * the target field. Chris@0: * @param {string} [replace=''] Chris@0: * A string to use as the replacement value for disallowed characters. Chris@0: * @param {string} [suffix=''] Chris@0: * A suffix to append at the end of the target field content. Chris@0: */ Chris@17: Drupal.viewsUi.FormFieldFiller = function($target, exclude, replace, suffix) { Chris@0: /** Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.source = $('#edit-label'); Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.target = $target; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {bool} Chris@0: */ Chris@0: this.exclude = exclude || false; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {string} Chris@0: */ Chris@0: this.replace = replace || ''; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {string} Chris@0: */ Chris@0: this.suffix = suffix || ''; Chris@0: Chris@0: // Create bound versions of this instance's object methods to use as event Chris@0: // handlers. This will let us easily unbind those specific handlers later Chris@0: // on. NOTE: jQuery.proxy will not work for this because it assumes we want Chris@0: // only one bound version of an object method, whereas we need one version Chris@0: // per object instance. Chris@0: const self = this; Chris@0: Chris@0: /** Chris@0: * Populate the target form field with the altered source field value. Chris@0: * Chris@0: * @return {*} Chris@0: * The result of the _populate call, which should be undefined. Chris@0: */ Chris@17: this.populate = function() { Chris@0: return self._populate.call(self); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Stop prepopulating the form fields. Chris@0: * Chris@0: * @return {*} Chris@0: * The result of the _unbind call, which should be undefined. Chris@0: */ Chris@17: this.unbind = function() { Chris@0: return self._unbind.call(self); Chris@0: }; Chris@0: Chris@0: this.bind(); Chris@0: // Object constructor; no return value. Chris@0: }; Chris@0: Chris@17: $.extend( Chris@17: Drupal.viewsUi.FormFieldFiller.prototype, Chris@17: /** @lends Drupal.viewsUi.FormFieldFiller# */ { Chris@17: /** Chris@17: * Bind the form-filling behavior. Chris@17: */ Chris@17: bind() { Chris@17: this.unbind(); Chris@17: // Populate the form field when the source changes. Chris@17: this.source.on('keyup.viewsUi change.viewsUi', this.populate); Chris@17: // Quit populating the field as soon as it gets focus. Chris@17: this.target.on('focus.viewsUi', this.unbind); Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Get the source form field value as altered by the passed-in parameters. Chris@17: * Chris@17: * @return {string} Chris@17: * The source form field value. Chris@17: */ Chris@17: getTransliterated() { Chris@17: let from = this.source.val(); Chris@17: if (this.exclude) { Chris@17: from = from.toLowerCase().replace(this.exclude, this.replace); Chris@17: } Chris@17: return from; Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Populate the target form field with the altered source field value. Chris@17: */ Chris@17: _populate() { Chris@17: const transliterated = this.getTransliterated(); Chris@17: const suffix = this.suffix; Chris@17: this.target.each(function(i) { Chris@17: // Ensure that the maxlength is not exceeded by prepopulating the field. Chris@17: const maxlength = $(this).attr('maxlength') - suffix.length; Chris@17: $(this).val(transliterated.substr(0, maxlength) + suffix); Chris@17: }); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Stop prepopulating the form fields. Chris@17: */ Chris@17: _unbind() { Chris@17: this.source.off('keyup.viewsUi change.viewsUi', this.populate); Chris@17: this.target.off('focus.viewsUi', this.unbind); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Bind event handlers to new form fields, after they're replaced via Ajax. Chris@17: * Chris@17: * @param {jQuery} $fields Chris@17: * Fields to rebind functionality to. Chris@17: */ Chris@17: rebind($fields) { Chris@17: this.target = $fields; Chris@17: this.bind(); Chris@17: }, Chris@0: }, Chris@17: ); Chris@0: Chris@0: /** Chris@0: * Adds functionality for the add item form. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches the functionality in {@link Drupal.viewsUi.AddItemForm} to the Chris@0: * forms in question. Chris@0: */ Chris@0: Drupal.behaviors.addItemForm = { Chris@0: attach(context) { Chris@0: const $context = $(context); Chris@0: let $form = $context; Chris@0: // The add handler form may have an id of views-ui-add-handler-form--n. Chris@0: if (!$context.is('form[id^="views-ui-add-handler-form"]')) { Chris@0: $form = $context.find('form[id^="views-ui-add-handler-form"]'); Chris@0: } Chris@0: if ($form.once('views-ui-add-handler-form').length) { Chris@0: // If we we have an unprocessed views-ui-add-handler-form, let's Chris@0: // instantiate. Chris@0: new Drupal.viewsUi.AddItemForm($form); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Constructs a new AddItemForm. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {jQuery} $form Chris@0: * The form element used. Chris@0: */ Chris@17: Drupal.viewsUi.AddItemForm = function($form) { Chris@0: /** Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.$form = $form; Chris@17: this.$form Chris@17: .find('.views-filterable-options :checkbox') Chris@17: .on('click', $.proxy(this.handleCheck, this)); Chris@0: Chris@0: /** Chris@0: * Find the wrapper of the displayed text. Chris@0: */ Chris@0: this.$selected_div = this.$form.find('.views-selected-options').parent(); Chris@0: this.$selected_div.hide(); Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {Array} Chris@0: */ Chris@0: this.checkedItems = []; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Handles a checkbox check. Chris@0: * Chris@0: * @param {jQuery.Event} event Chris@0: * The event triggered. Chris@0: */ Chris@17: Drupal.viewsUi.AddItemForm.prototype.handleCheck = function(event) { Chris@0: const $target = $(event.target); Chris@17: const label = $.trim( Chris@17: $target Chris@17: .closest('td') Chris@17: .next() Chris@17: .html(), Chris@17: ); Chris@0: // Add/remove the checked item to the list. Chris@0: if ($target.is(':checked')) { Chris@0: this.$selected_div.show().css('display', 'block'); Chris@0: this.checkedItems.push(label); Chris@17: } else { Chris@0: const position = $.inArray(label, this.checkedItems); Chris@0: // Delete the item from the list and make sure that the list doesn't have Chris@0: // undefined items left. Chris@0: for (let i = 0; i < this.checkedItems.length; i++) { Chris@0: if (i === position) { Chris@0: this.checkedItems.splice(i, 1); Chris@0: i--; Chris@0: break; Chris@0: } Chris@0: } Chris@0: // Hide it again if none item is selected. Chris@0: if (this.checkedItems.length === 0) { Chris@0: this.$selected_div.hide(); Chris@0: } Chris@0: } Chris@0: this.refreshCheckedItems(); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Refresh the display of the checked items. Chris@0: */ Chris@17: Drupal.viewsUi.AddItemForm.prototype.refreshCheckedItems = function() { Chris@0: // Perhaps we should precache the text div, too. Chris@17: this.$selected_div Chris@17: .find('.views-selected-options') Chris@0: .html(this.checkedItems.join(', ')) Chris@0: .trigger('dialogContentResize'); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * The input field items that add displays must be rendered as `` Chris@0: * elements. The following behavior detaches the `` elements from the Chris@0: * DOM, wraps them in an unordered list, then appends them to the list of Chris@0: * tabs. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Fixes the input elements needed. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiRenderAddViewButton = { Chris@0: attach(context) { Chris@0: // Build the add display menu and pull the display input buttons into it. Chris@17: const $menu = $(context) Chris@17: .find('#views-display-menu-tabs') Chris@17: .once('views-ui-render-add-view-button'); Chris@0: if (!$menu.length) { Chris@0: return; Chris@0: } Chris@0: Chris@17: const $addDisplayDropdown = $( Chris@17: `
  • ${Drupal.t( Chris@17: 'Add', Chris@17: )}
  • `, Chris@17: ); Chris@0: const $displayButtons = $menu.nextAll('input.add-display').detach(); Chris@14: $displayButtons Chris@14: .appendTo($addDisplayDropdown.find('.action-list')) Chris@14: .wrap('
  • ') Chris@14: .parent() Chris@14: .eq(0) Chris@14: .addClass('first') Chris@14: .end() Chris@14: .eq(-1) Chris@14: .addClass('last'); Chris@0: // Remove the 'Add ' prefix from the button labels since they're being Chris@0: // placed in an 'Add' dropdown. @todo This assumes English, but so does Chris@0: // $addDisplayDropdown above. Add support for translation. Chris@17: $displayButtons.each(function() { Chris@0: const label = $(this).val(); Chris@0: if (label.substr(0, 4) === 'Add ') { Chris@0: $(this).val(label.substr(4)); Chris@0: } Chris@0: }); Chris@0: $addDisplayDropdown.appendTo($menu); Chris@0: Chris@0: // Add the click handler for the add display button. Chris@17: $menu.find('li.add > a').on('click', function(event) { Chris@0: event.preventDefault(); Chris@0: const $trigger = $(this); Chris@0: Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger); Chris@0: }); Chris@0: // Add a mouseleave handler to close the dropdown when the user mouses Chris@0: // away from the item. We use mouseleave instead of mouseout because Chris@0: // the user is going to trigger mouseout when she moves from the trigger Chris@0: // link to the sub menu items. Chris@0: // We use the live binder because the open class on this item will be Chris@0: // toggled on and off and we want the handler to take effect in the cases Chris@0: // that the class is present, but not when it isn't. Chris@17: $('li.add', $menu).on('mouseleave', function(event) { Chris@0: const $this = $(this); Chris@0: const $trigger = $this.children('a[href="#"]'); Chris@0: if ($this.children('.action-list').is(':visible')) { Chris@0: Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu($trigger); Chris@0: } Chris@0: }); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Toggle menu visibility. Chris@0: * Chris@0: * @param {jQuery} $trigger Chris@0: * The element where the toggle was triggered. Chris@0: * Chris@0: * Chris@0: * @note [@jessebeach] I feel like the following should be a more generic Chris@0: * function and not written specifically for this UI, but I'm not sure Chris@0: * where to put it. Chris@0: */ Chris@17: Drupal.behaviors.viewsUiRenderAddViewButton.toggleMenu = function($trigger) { Chris@0: $trigger.parent().toggleClass('open'); Chris@0: $trigger.next().slideToggle('fast'); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Add search options to the views ui. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches {@link Drupal.viewsUi.OptionsSearch} to the views ui filter Chris@0: * options. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiSearchOptions = { Chris@0: attach(context) { Chris@0: const $context = $(context); Chris@0: let $form = $context; Chris@0: // The add handler form may have an id of views-ui-add-handler-form--n. Chris@0: if (!$context.is('form[id^="views-ui-add-handler-form"]')) { Chris@0: $form = $context.find('form[id^="views-ui-add-handler-form"]'); Chris@0: } Chris@0: // Make sure we don't add more than one event handler to the same form. Chris@0: if ($form.once('views-ui-filter-options').length) { Chris@0: new Drupal.viewsUi.OptionsSearch($form); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Constructor for the viewsUi.OptionsSearch object. Chris@0: * Chris@0: * The OptionsSearch object filters the available options on a form according Chris@0: * to the user's search term. Typing in "taxonomy" will show only those Chris@0: * options containing "taxonomy" in their label. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {jQuery} $form Chris@0: * The form element. Chris@0: */ Chris@17: Drupal.viewsUi.OptionsSearch = function($form) { Chris@0: /** Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.$form = $form; Chris@0: Chris@0: // Click on the title checks the box. Chris@17: this.$form.on('click', 'td.title', event => { Chris@0: const $target = $(event.currentTarget); Chris@17: $target Chris@17: .closest('tr') Chris@17: .find('input') Chris@17: .trigger('click'); Chris@0: }); Chris@0: Chris@17: const searchBoxSelector = Chris@17: '[data-drupal-selector="edit-override-controls-options-search"]'; Chris@17: const controlGroupSelector = Chris@17: '[data-drupal-selector="edit-override-controls-group"]'; Chris@17: this.$form.on( Chris@17: 'formUpdated', Chris@17: `${searchBoxSelector},${controlGroupSelector}`, Chris@17: $.proxy(this.handleFilter, this), Chris@17: ); Chris@0: Chris@0: this.$searchBox = this.$form.find(searchBoxSelector); Chris@0: this.$controlGroup = this.$form.find(controlGroupSelector); Chris@0: Chris@0: /** Chris@0: * Get a list of option labels and their corresponding divs and maintain it Chris@0: * in memory, so we have as little overhead as possible at keyup time. Chris@0: */ Chris@0: this.options = this.getOptions(this.$form.find('.filterable-option')); Chris@0: Chris@0: // Trap the ENTER key in the search box so that it doesn't submit the form. Chris@17: this.$searchBox.on('keypress', event => { Chris@0: if (event.which === 13) { Chris@0: event.preventDefault(); Chris@0: } Chris@0: }); Chris@0: }; Chris@0: Chris@17: $.extend( Chris@17: Drupal.viewsUi.OptionsSearch.prototype, Chris@17: /** @lends Drupal.viewsUi.OptionsSearch# */ { Chris@17: /** Chris@17: * Assemble a list of all the filterable options on the form. Chris@17: * Chris@17: * @param {jQuery} $allOptions Chris@17: * A jQuery object representing the rows of filterable options to be Chris@17: * shown and hidden depending on the user's search terms. Chris@17: * Chris@17: * @return {Array} Chris@17: * An array of all the filterable options. Chris@17: */ Chris@17: getOptions($allOptions) { Chris@17: let $title; Chris@17: let $description; Chris@17: let $option; Chris@17: const options = []; Chris@17: const length = $allOptions.length; Chris@17: for (let i = 0; i < length; i++) { Chris@17: $option = $($allOptions[i]); Chris@17: $title = $option.find('.title'); Chris@17: $description = $option.find('.description'); Chris@17: options[i] = { Chris@17: // Search on the lowercase version of the title text + description. Chris@17: searchText: `${$title Chris@17: .text() Chris@17: .toLowerCase()} ${$description.text().toLowerCase()}`, Chris@17: // Maintain a reference to the jQuery object for each row, so we don't Chris@17: // have to create a new object inside the performance-sensitive keyup Chris@17: // handler. Chris@17: $div: $option, Chris@17: }; Chris@17: } Chris@17: return options; Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Filter handler for the search box and type select that hides or shows the relevant Chris@17: * options. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The formUpdated event. Chris@17: */ Chris@17: handleFilter(event) { Chris@17: // Determine the user's search query. The search text has been converted Chris@17: // to lowercase. Chris@17: const search = this.$searchBox.val().toLowerCase(); Chris@17: const words = search.split(' '); Chris@17: // Get selected Group Chris@17: const group = this.$controlGroup.val(); Chris@17: Chris@17: // Search through the search texts in the form for matching text. Chris@17: this.options.forEach(option => { Chris@17: function hasWord(word) { Chris@17: return option.searchText.indexOf(word) !== -1; Chris@17: } Chris@17: Chris@17: let found = true; Chris@17: // Each word in the search string has to match the item in order for the Chris@17: // item to be shown. Chris@17: if (search) { Chris@17: found = words.every(hasWord); Chris@17: } Chris@17: if (found && group !== 'all') { Chris@17: found = option.$div.hasClass(group); Chris@17: } Chris@17: Chris@17: option.$div.toggle(found); Chris@17: }); Chris@17: Chris@17: // Adapt dialog to content size. Chris@17: $(event.target).trigger('dialogContentResize'); Chris@17: }, Chris@0: }, Chris@17: ); Chris@0: Chris@0: /** Chris@0: * Preview functionality in the views edit form. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches the preview functionality to the view edit form. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiPreview = { Chris@0: attach(context) { Chris@0: // Only act on the edit view form. Chris@17: const $contextualFiltersBucket = $(context).find( Chris@17: '.views-display-column .views-ui-display-tab-bucket.argument', Chris@17: ); Chris@0: if ($contextualFiltersBucket.length === 0) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // If the display has no contextual filters, hide the form where you Chris@0: // enter the contextual filters for the live preview. If it has contextual Chris@0: // filters, show the form. Chris@17: const $contextualFilters = $contextualFiltersBucket.find( Chris@17: '.views-display-setting a', Chris@17: ); Chris@0: if ($contextualFilters.length) { Chris@17: $('#preview-args') Chris@17: .parent() Chris@17: .show(); Chris@17: } else { Chris@17: $('#preview-args') Chris@17: .parent() Chris@17: .hide(); Chris@0: } Chris@0: Chris@0: // Executes an initial preview. Chris@17: if ( Chris@17: $('#edit-displays-live-preview') Chris@17: .once('edit-displays-live-preview') Chris@17: .is(':checked') Chris@17: ) { Chris@17: $('#preview-submit') Chris@17: .once('edit-displays-live-preview') Chris@17: .trigger('click'); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Rearranges the filters. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@17: * Attach handlers to make it possible to rearrange the filters in the form Chris@0: * in question. Chris@0: * @see Drupal.viewsUi.RearrangeFilterHandler Chris@0: */ Chris@0: Drupal.behaviors.viewsUiRearrangeFilter = { Chris@0: attach(context) { Chris@0: // Only act on the rearrange filter form. Chris@17: if ( Chris@17: typeof Drupal.tableDrag === 'undefined' || Chris@17: typeof Drupal.tableDrag['views-rearrange-filters'] === 'undefined' Chris@17: ) { Chris@0: return; Chris@0: } Chris@0: const $context = $(context); Chris@17: const $table = $context Chris@17: .find('#views-rearrange-filters') Chris@17: .once('views-rearrange-filters'); Chris@17: const $operator = $context Chris@17: .find('.js-form-item-filter-groups-operator') Chris@17: .once('views-rearrange-filters'); Chris@0: if ($table.length) { Chris@0: new Drupal.viewsUi.RearrangeFilterHandler($table, $operator); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Improve the UI of the rearrange filters dialog box. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {jQuery} $table Chris@0: * The table in the filter form. Chris@0: * @param {jQuery} $operator Chris@0: * The filter groups operator element. Chris@0: */ Chris@17: Drupal.viewsUi.RearrangeFilterHandler = function($table, $operator) { Chris@0: /** Chris@0: * Keep a reference to the `` being altered and to the div containing Chris@0: * the filter groups operator dropdown (if it exists). Chris@0: */ Chris@0: this.table = $table; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.operator = $operator; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {bool} Chris@0: */ Chris@0: this.hasGroupOperator = this.operator.length > 0; Chris@0: Chris@0: /** Chris@0: * Keep a reference to all draggable rows within the table. Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.draggableRows = $table.find('.draggable'); Chris@0: Chris@0: /** Chris@0: * Keep a reference to the buttons for adding and removing filter groups. Chris@0: * Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.addGroupButton = $('input#views-add-group'); Chris@0: Chris@0: /** Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.removeGroupButtons = $table.find('input.views-remove-group'); Chris@0: Chris@0: // Add links that duplicate the functionality of the (hidden) add and remove Chris@0: // buttons. Chris@0: this.insertAddRemoveFilterGroupLinks(); Chris@0: Chris@0: // When there is a filter groups operator dropdown on the page, create Chris@0: // duplicates of the dropdown between each pair of filter groups. Chris@0: if (this.hasGroupOperator) { Chris@0: /** Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.dropdowns = this.duplicateGroupsOperator(); Chris@0: this.syncGroupsOperators(); Chris@0: } Chris@0: Chris@0: // Add methods to the tableDrag instance to account for operator cells Chris@0: // (which span multiple rows), the operator labels next to each filter Chris@0: // (e.g., "And" or "Or"), the filter groups, and other special aspects of Chris@0: // this tableDrag instance. Chris@0: this.modifyTableDrag(); Chris@0: Chris@0: // Initialize the operator labels (e.g., "And" or "Or") that are displayed Chris@0: // next to the filters in each group, and bind a handler so that they change Chris@0: // based on the values of the operator dropdown within that group. Chris@0: this.redrawOperatorLabels(); Chris@17: $table Chris@17: .find('.views-group-title select') Chris@0: .once('views-rearrange-filter-handler') Chris@17: .on( Chris@17: 'change.views-rearrange-filter-handler', Chris@17: $.proxy(this, 'redrawOperatorLabels'), Chris@17: ); Chris@0: Chris@0: // Bind handlers so that when a "Remove" link is clicked, we: Chris@0: // - Update the rowspans of cells containing an operator dropdown (since Chris@0: // they need to change to reflect the number of rows in each group). Chris@0: // - Redraw the operator labels next to the filters in the group (since the Chris@0: // filter that is currently displayed last in each group is not supposed Chris@0: // to have a label display next to it). Chris@17: $table Chris@17: .find('a.views-groups-remove-link') Chris@0: .once('views-rearrange-filter-handler') Chris@17: .on( Chris@17: 'click.views-rearrange-filter-handler', Chris@17: $.proxy(this, 'updateRowspans'), Chris@17: ) Chris@17: .on( Chris@17: 'click.views-rearrange-filter-handler', Chris@17: $.proxy(this, 'redrawOperatorLabels'), Chris@17: ); Chris@0: }; Chris@0: Chris@17: $.extend( Chris@17: Drupal.viewsUi.RearrangeFilterHandler.prototype, Chris@17: /** @lends Drupal.viewsUi.RearrangeFilterHandler# */ { Chris@17: /** Chris@17: * Insert links that allow filter groups to be added and removed. Chris@17: */ Chris@17: insertAddRemoveFilterGroupLinks() { Chris@17: // Insert a link for adding a new group at the top of the page, and make Chris@17: // it match the action link styling used in a typical page.html.twig. Chris@17: // Since Drupal does not provide a theme function for this markup this is Chris@17: // the best we can do. Chris@17: $( Chris@17: ``, Chris@17: ) Chris@17: .prependTo(this.table.parent()) Chris@17: // When the link is clicked, dynamically click the hidden form button Chris@17: // for adding a new filter group. Chris@17: .once('views-rearrange-filter-handler') Chris@17: .find('#views-add-group-link') Chris@17: .on( Chris@17: 'click.views-rearrange-filter-handler', Chris@17: $.proxy(this, 'clickAddGroupButton'), Chris@17: ); Chris@0: Chris@17: // Find each (visually hidden) button for removing a filter group and Chris@17: // insert a link next to it. Chris@17: const length = this.removeGroupButtons.length; Chris@17: let i; Chris@17: for (i = 0; i < length; i++) { Chris@17: const $removeGroupButton = $(this.removeGroupButtons[i]); Chris@17: const buttonId = $removeGroupButton.attr('id'); Chris@17: $( Chris@17: `${Drupal.t( Chris@17: 'Remove group', Chris@17: )}`, Chris@17: ) Chris@17: .insertBefore($removeGroupButton) Chris@17: // When the link is clicked, dynamically click the corresponding form Chris@17: // button. Chris@17: .once('views-rearrange-filter-handler') Chris@17: .on( Chris@17: 'click.views-rearrange-filter-handler', Chris@17: { buttonId }, Chris@17: $.proxy(this, 'clickRemoveGroupButton'), Chris@17: ); Chris@17: } Chris@17: }, Chris@0: Chris@0: /** Chris@17: * Dynamically click the button that adds a new filter group. Chris@0: * Chris@17: * @param {jQuery.Event} event Chris@17: * The event triggered. Chris@0: */ Chris@17: clickAddGroupButton(event) { Chris@17: this.addGroupButton.trigger('mousedown'); Chris@17: event.preventDefault(); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Dynamically click a button for removing a filter group. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * Event being triggered, with event.data.buttonId set to the ID of the Chris@17: * form button that should be clicked. Chris@17: */ Chris@17: clickRemoveGroupButton(event) { Chris@17: this.table.find(`#${event.data.buttonId}`).trigger('mousedown'); Chris@17: event.preventDefault(); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Move the groups operator so that it's between the first two groups, and Chris@17: * duplicate it between any subsequent groups. Chris@17: * Chris@17: * @return {jQuery} Chris@17: * An operator element. Chris@17: */ Chris@17: duplicateGroupsOperator() { Chris@17: let newRow; Chris@17: let titleRow; Chris@17: Chris@17: const titleRows = $('tr.views-group-title').once( Chris@17: 'duplicateGroupsOperator', Chris@17: ); Chris@17: Chris@17: if (!titleRows.length) { Chris@17: return this.operator; Chris@17: } Chris@17: Chris@17: // Get rid of the explanatory text around the operator; its placement is Chris@17: // explanatory enough. Chris@17: this.operator Chris@17: .find('label') Chris@17: .add('div.description') Chris@17: .addClass('visually-hidden'); Chris@17: this.operator.find('select').addClass('form-select'); Chris@17: Chris@17: // Keep a list of the operator dropdowns, so we can sync their behavior Chris@17: // later. Chris@17: const dropdowns = this.operator; Chris@17: Chris@17: // Move the operator to a new row just above the second group. Chris@17: titleRow = $('tr#views-group-title-2'); Chris@17: newRow = $( Chris@17: '', Chris@17: ); Chris@17: newRow.find('td').append(this.operator); Chris@17: newRow.insertBefore(titleRow); Chris@17: const length = titleRows.length; Chris@17: // Starting with the third group, copy the operator to a new row above the Chris@17: // group title. Chris@17: for (let i = 2; i < length; i++) { Chris@17: titleRow = $(titleRows[i]); Chris@17: // Make a copy of the operator dropdown and put it in a new table row. Chris@17: const fakeOperator = this.operator.clone(); Chris@17: fakeOperator.attr('id', ''); Chris@17: newRow = $( Chris@17: '', Chris@17: ); Chris@17: newRow.find('td').append(fakeOperator); Chris@17: newRow.insertBefore(titleRow); Chris@17: dropdowns.add(fakeOperator); Chris@17: } Chris@17: Chris@17: return dropdowns; Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Make the duplicated groups operators change in sync with each other. Chris@17: */ Chris@17: syncGroupsOperators() { Chris@17: if (this.dropdowns.length < 2) { Chris@17: // We only have one dropdown (or none at all), so there's nothing to Chris@17: // sync. Chris@17: return; Chris@17: } Chris@17: Chris@17: this.dropdowns.on('change', $.proxy(this, 'operatorChangeHandler')); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Click handler for the operators that appear between filter groups. Chris@17: * Chris@17: * Forces all operator dropdowns to have the same value. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The event triggered. Chris@17: */ Chris@17: operatorChangeHandler(event) { Chris@17: const $target = $(event.target); Chris@17: const operators = this.dropdowns.find('select').not($target); Chris@17: Chris@17: // Change the other operators to match this new value. Chris@17: operators.val($target.val()); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * @method Chris@17: */ Chris@17: modifyTableDrag() { Chris@17: const tableDrag = Drupal.tableDrag['views-rearrange-filters']; Chris@17: const filterHandler = this; Chris@17: Chris@17: /** Chris@17: * Override the row.onSwap method from tabledrag.js. Chris@17: * Chris@17: * When a row is dragged to another place in the table, several things Chris@17: * need to occur. Chris@17: * - The row needs to be moved so that it's within one of the filter Chris@17: * groups. Chris@17: * - The operator cells that span multiple rows need their rowspan Chris@17: * attributes updated to reflect the number of rows in each group. Chris@17: * - The operator labels that are displayed next to each filter need to Chris@17: * be redrawn, to account for the row's new location. Chris@17: */ Chris@17: tableDrag.row.prototype.onSwap = function() { Chris@17: if (filterHandler.hasGroupOperator) { Chris@17: // Make sure the row that just got moved (this.group) is inside one Chris@17: // of the filter groups (i.e. below an empty marker row or a Chris@17: // draggable). If it isn't, move it down one. Chris@17: const thisRow = $(this.group); Chris@17: const previousRow = thisRow.prev('tr'); Chris@17: if ( Chris@17: previousRow.length && Chris@17: !previousRow.hasClass('group-message') && Chris@17: !previousRow.hasClass('draggable') Chris@17: ) { Chris@17: // Move the dragged row down one. Chris@17: const next = thisRow.next(); Chris@17: if (next.is('tr')) { Chris@17: this.swap('after', next); Chris@17: } Chris@17: } Chris@17: filterHandler.updateRowspans(); Chris@17: } Chris@17: // Redraw the operator labels that are displayed next to each filter, to Chris@17: // account for the row's new location. Chris@17: filterHandler.redrawOperatorLabels(); Chris@17: }; Chris@17: Chris@17: /** Chris@17: * Override the onDrop method from tabledrag.js. Chris@17: */ Chris@17: tableDrag.onDrop = function() { Chris@17: // If the tabledrag change marker (i.e., the "*") has been inserted Chris@17: // inside a row after the operator label (i.e., "And" or "Or") Chris@17: // rearrange the items so the operator label continues to appear last. Chris@17: const changeMarker = $(this.oldRowElement).find('.tabledrag-changed'); Chris@17: if (changeMarker.length) { Chris@17: // Search for occurrences of the operator label before the change Chris@17: // marker, and reverse them. Chris@17: const operatorLabel = changeMarker.prevAll('.views-operator-label'); Chris@17: if (operatorLabel.length) { Chris@17: operatorLabel.insertAfter(changeMarker); Chris@0: } Chris@0: } Chris@17: Chris@17: // Make sure the "group" dropdown is properly updated when rows are Chris@17: // dragged into an empty filter group. This is borrowed heavily from Chris@17: // the block.js implementation of tableDrag.onDrop(). Chris@17: const groupRow = $(this.rowObject.element) Chris@17: .prevAll('tr.group-message') Chris@17: .get(0); Chris@17: const groupName = groupRow.className.replace( Chris@17: /([^ ]+[ ]+)*group-([^ ]+)-message([ ]+[^ ]+)*/, Chris@17: '$2', Chris@17: ); Chris@17: const groupField = $( Chris@17: 'select.views-group-select', Chris@17: this.rowObject.element, Chris@17: ); Chris@18: if (!groupField.is(`.views-group-select-${groupName}`)) { Chris@17: const oldGroupName = groupField Chris@17: .attr('class') Chris@17: .replace( Chris@17: /([^ ]+[ ]+)*views-group-select-([^ ]+)([ ]+[^ ]+)*/, Chris@17: '$2', Chris@17: ); Chris@17: groupField Chris@17: .removeClass(`views-group-select-${oldGroupName}`) Chris@17: .addClass(`views-group-select-${groupName}`); Chris@17: groupField.val(groupName); Chris@17: } Chris@17: }; Chris@17: }, Chris@0: Chris@0: /** Chris@17: * Redraw the operator labels that are displayed next to each filter. Chris@0: */ Chris@17: redrawOperatorLabels() { Chris@17: for (let i = 0; i < this.draggableRows.length; i++) { Chris@17: // Within the row, the operator labels are displayed inside the first Chris@17: // table cell (next to the filter name). Chris@17: const $draggableRow = $(this.draggableRows[i]); Chris@17: const $firstCell = $draggableRow.find('td').eq(0); Chris@17: if ($firstCell.length) { Chris@17: // The value of the operator label ("And" or "Or") is taken from the Chris@17: // first operator dropdown we encounter, going backwards from the Chris@17: // current row. This dropdown is the one associated with the current Chris@17: // row's filter group. Chris@17: const operatorValue = $draggableRow Chris@17: .prevAll('.views-group-title') Chris@17: .find('option:selected') Chris@17: .html(); Chris@17: const operatorLabel = `${operatorValue}`; Chris@17: // If the next visible row after this one is a draggable filter row, Chris@17: // display the operator label next to the current row. (Checking for Chris@17: // visibility is necessary here since the "Remove" links hide the Chris@17: // removed row but don't actually remove it from the document). Chris@17: const $nextRow = $draggableRow.nextAll(':visible').eq(0); Chris@17: const $existingOperatorLabel = $firstCell.find( Chris@17: '.views-operator-label', Chris@17: ); Chris@17: if ($nextRow.hasClass('draggable')) { Chris@17: // If an operator label was already there, replace it with the new Chris@17: // one. Chris@17: if ($existingOperatorLabel.length) { Chris@17: $existingOperatorLabel.replaceWith(operatorLabel); Chris@17: } Chris@17: // Otherwise, append the operator label to the end of the table Chris@17: // cell. Chris@17: else { Chris@17: $firstCell.append(operatorLabel); Chris@17: } Chris@17: } Chris@17: // If the next row doesn't contain a filter, then this is the last row Chris@17: // in the group. We don't want to display the operator there (since Chris@17: // operators should only display between two related filters, e.g. Chris@17: // "filter1 AND filter2 AND filter3"). So we remove any existing label Chris@17: // that this row has. Chris@17: else { Chris@17: $existingOperatorLabel.remove(); Chris@17: } Chris@0: } Chris@0: } Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Update the rowspan attribute of each cell containing an operator Chris@17: * dropdown. Chris@17: */ Chris@17: updateRowspans() { Chris@17: let $row; Chris@17: let $currentEmptyRow; Chris@17: let draggableCount; Chris@17: let $operatorCell; Chris@17: const rows = $(this.table).find('tr'); Chris@17: const length = rows.length; Chris@17: for (let i = 0; i < length; i++) { Chris@17: $row = $(rows[i]); Chris@17: if ($row.hasClass('views-group-title')) { Chris@17: // This row is a title row. Chris@17: // Keep a reference to the cell containing the dropdown operator. Chris@17: $operatorCell = $row.find('td.group-operator'); Chris@17: // Assume this filter group is empty, until we find otherwise. Chris@17: draggableCount = 0; Chris@17: $currentEmptyRow = $row.next('tr'); Chris@17: $currentEmptyRow Chris@17: .removeClass('group-populated') Chris@17: .addClass('group-empty'); Chris@17: // The cell with the dropdown operator should span the title row and Chris@17: // the "this group is empty" row. Chris@17: $operatorCell.attr('rowspan', 2); Chris@17: } else if ($row.hasClass('draggable') && $row.is(':visible')) { Chris@17: // We've found a visible filter row, so we now know the group isn't Chris@17: // empty. Chris@17: draggableCount++; Chris@17: $currentEmptyRow Chris@17: .removeClass('group-empty') Chris@17: .addClass('group-populated'); Chris@17: // The operator cell should span all draggable rows, plus the title. Chris@17: $operatorCell.attr('rowspan', draggableCount + 1); Chris@0: } Chris@0: } Chris@17: }, Chris@0: }, Chris@17: ); Chris@0: Chris@0: /** Chris@0: * Add a select all checkbox, which checks each checkbox at once. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches select all functionality to the views filter form. Chris@0: */ Chris@0: Drupal.behaviors.viewsFilterConfigSelectAll = { Chris@0: attach(context) { Chris@0: const $context = $(context); Chris@0: Chris@17: const $selectAll = $context Chris@17: .find('.js-form-item-options-value-all') Chris@17: .once('filterConfigSelectAll'); Chris@0: const $selectAllCheckbox = $selectAll.find('input[type=checkbox]'); Chris@17: const $checkboxes = $selectAll Chris@17: .closest('.form-checkboxes') Chris@17: .find( Chris@17: '.js-form-type-checkbox:not(.js-form-item-options-value-all) input[type="checkbox"]', Chris@17: ); Chris@0: Chris@0: if ($selectAll.length) { Chris@17: // Show the select all checkbox. Chris@0: $selectAll.show(); Chris@17: $selectAllCheckbox.on('click', function() { Chris@0: // Update all checkbox beside the select all checkbox. Chris@0: $checkboxes.prop('checked', $(this).is(':checked')); Chris@0: }); Chris@0: Chris@0: // Uncheck the select all checkbox if any of the others are unchecked. Chris@17: $checkboxes.on('click', function() { Chris@0: if ($(this).is('checked') === false) { Chris@0: $selectAllCheckbox.prop('checked', false); Chris@0: } Chris@0: }); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Remove icon class from elements that are themed as buttons or dropbuttons. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Removes the icon class from certain views elements. Chris@0: */ Chris@0: Drupal.behaviors.viewsRemoveIconClass = { Chris@0: attach(context) { Chris@14: $(context) Chris@14: .find('.dropbutton') Chris@14: .once('dropbutton-icon') Chris@14: .find('.icon') Chris@14: .removeClass('icon'); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Change "Expose filter" buttons into checkboxes. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Changes buttons into checkboxes via {@link Drupal.viewsUi.Checkboxifier}. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiCheckboxify = { Chris@0: attach(context, settings) { Chris@17: const $buttons = $( Chris@17: '[data-drupal-selector="edit-options-expose-button-button"], [data-drupal-selector="edit-options-group-button-button"]', Chris@17: ).once('views-ui-checkboxify'); Chris@0: const length = $buttons.length; Chris@0: let i; Chris@0: for (i = 0; i < length; i++) { Chris@0: new Drupal.viewsUi.Checkboxifier($buttons[i]); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Change the default widget to select the default group according to the Chris@0: * selected widget for the exposed group. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Changes the default widget based on user input. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiChangeDefaultWidget = { Chris@0: attach(context) { Chris@0: const $context = $(context); Chris@0: Chris@0: function changeDefaultWidget(event) { Chris@0: if ($(event.target).prop('checked')) { Chris@17: $context Chris@17: .find('input.default-radios') Chris@17: .parent() Chris@17: .hide(); Chris@17: $context Chris@17: .find('td.any-default-radios-row') Chris@17: .parent() Chris@17: .hide(); Chris@17: $context Chris@17: .find('input.default-checkboxes') Chris@17: .parent() Chris@17: .show(); Chris@17: } else { Chris@17: $context Chris@17: .find('input.default-checkboxes') Chris@17: .parent() Chris@17: .hide(); Chris@17: $context Chris@17: .find('td.any-default-radios-row') Chris@17: .parent() Chris@17: .show(); Chris@17: $context Chris@17: .find('input.default-radios') Chris@17: .parent() Chris@17: .show(); Chris@0: } Chris@0: } Chris@0: Chris@0: // Update on widget change. Chris@17: $context Chris@17: .find('input[name="options[group_info][multiple]"]') Chris@0: .on('change', changeDefaultWidget) Chris@0: // Update the first time the form is rendered. Chris@0: .trigger('change'); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Attaches expose filter button to a checkbox that triggers its click event. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {HTMLElement} button Chris@0: * The DOM object representing the button to be checkboxified. Chris@0: */ Chris@17: Drupal.viewsUi.Checkboxifier = function(button) { Chris@0: this.$button = $(button); Chris@0: this.$parent = this.$button.parent('div.views-expose, div.views-grouped'); Chris@0: this.$input = this.$parent.find('input:checkbox, input:radio'); Chris@0: // Hide the button and its description. Chris@0: this.$button.hide(); Chris@0: this.$parent.find('.exposed-description, .grouped-description').hide(); Chris@0: Chris@0: this.$input.on('click', $.proxy(this, 'clickHandler')); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * When the checkbox is checked or unchecked, simulate a button press. Chris@0: * Chris@0: * @param {jQuery.Event} e Chris@0: * The event triggered. Chris@0: */ Chris@17: Drupal.viewsUi.Checkboxifier.prototype.clickHandler = function(e) { Chris@17: this.$button.trigger('click').trigger('submit'); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Change the Apply button text based upon the override select state. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches behavior to change the Apply button according to the current Chris@0: * state. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiOverrideSelect = { Chris@0: attach(context) { Chris@17: $(context) Chris@17: .find('[data-drupal-selector="edit-override-dropdown"]') Chris@17: .once('views-ui-override-button-text') Chris@17: .each(function() { Chris@17: // Closures! :( Chris@17: const $context = $(context); Chris@17: const $submit = $context.find('[id^=edit-submit]'); Chris@17: const oldValue = $submit.val(); Chris@0: Chris@17: $submit Chris@17: .once('views-ui-override-button-text') Chris@17: .on('mouseup', function() { Chris@17: $(this).val(oldValue); Chris@17: return true; Chris@17: }); Chris@0: Chris@17: $(this) Chris@17: .on('change', function() { Chris@17: const $this = $(this); Chris@17: if ($this.val() === 'default') { Chris@17: $submit.val(Drupal.t('Apply (all displays)')); Chris@17: } else if ($this.val() === 'default_revert') { Chris@17: $submit.val(Drupal.t('Revert to default')); Chris@17: } else { Chris@17: $submit.val(Drupal.t('Apply (this display)')); Chris@17: } Chris@17: const $dialog = $context.closest('.ui-dialog-content'); Chris@17: $dialog.trigger('dialogButtonsChange'); Chris@17: }) Chris@17: .trigger('change'); Chris@17: }); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Functionality for the remove link in the views UI. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches behavior for the remove view and remove display links. Chris@0: */ Chris@0: Drupal.behaviors.viewsUiHandlerRemoveLink = { Chris@0: attach(context) { Chris@0: const $context = $(context); Chris@0: // Handle handler deletion by looking for the hidden checkbox and hiding Chris@0: // the row. Chris@17: $context Chris@17: .find('a.views-remove-link') Chris@17: .once('views') Chris@17: .on('click', function(event) { Chris@17: const id = $(this) Chris@17: .attr('id') Chris@17: .replace('views-remove-link-', ''); Chris@17: $context.find(`#views-row-${id}`).hide(); Chris@17: $context.find(`#views-removed-${id}`).prop('checked', true); Chris@17: event.preventDefault(); Chris@17: }); Chris@0: Chris@0: // Handle display deletion by looking for the hidden checkbox and hiding Chris@0: // the row. Chris@17: $context Chris@17: .find('a.display-remove-link') Chris@17: .once('display') Chris@17: .on('click', function(event) { Chris@17: const id = $(this) Chris@17: .attr('id') Chris@17: .replace('display-remove-link-', ''); Chris@17: $context.find(`#display-row-${id}`).hide(); Chris@17: $context.find(`#display-removed-${id}`).prop('checked', true); Chris@17: event.preventDefault(); Chris@17: }); Chris@0: }, Chris@0: }; Chris@17: })(jQuery, Drupal, drupalSettings);