summaryrefslogtreecommitdiffstatshomepage
path: root/core/misc/dialog/dialog.ajax.js
blob: 0ff6d09fcffd10c024da524ea37a5e3049ad0370 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/**
 * @file
 * Extends the Drupal AJAX functionality to integrate the dialog API.
 */

(function ($, Drupal, { focusable }) {
  /**
   * Initialize dialogs for Ajax purposes.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches the behaviors for dialog ajax functionality.
   */
  Drupal.behaviors.dialog = {
    attach(context, settings) {
      // Provide a known 'drupal-modal' DOM element for Drupal-based modal
      // dialogs. Non-modal dialogs are responsible for creating their own
      // elements, since there can be multiple non-modal dialogs at a time.
      if (!document.querySelector('#drupal-modal')) {
        // Add 'ui-front' jQuery UI class so jQuery UI widgets like autocomplete
        // sit on top of dialogs. For more information see
        // http://api.jqueryui.com/theming/stacking-elements/.
        document.body.insertAdjacentHTML(
          'beforeend',
          '<div id="drupal-modal" class="ui-front" style="display:none"></div>',
        );
      }

      // Special behaviors specific when attaching content within a dialog.
      // These behaviors usually fire after a validation error inside a dialog.
      if (context !== document) {
        const dialog = context.closest('.ui-dialog-content');
        if (dialog) {
          // Remove and replace the dialog buttons with those from the new form.
          if ($(dialog).dialog('option', 'drupalAutoButtons')) {
            // Trigger an event to detect/sync changes to buttons.
            dialog.dispatchEvent(new CustomEvent('dialogButtonsChange'));
          }

          setTimeout(function () {
            // Account for pre-existing focus handling that may have already moved
            // the focus inside the dialog.
            if (!dialog.contains(document.activeElement)) {
              // Move focus to the first focusable element in the next event loop
              // to allow dialog buttons to be changed first.
              $(dialog).dialog('instance')._focusedElement = null;
              $(dialog).dialog('instance')._focusTabbable();
            }
          }, 0);
        }
      }

      const originalClose = settings.dialog.close;
      // Overwrite the close method to remove the dialog on closing.
      settings.dialog.close = function (event, ...args) {
        originalClose.apply(settings.dialog, [event, ...args]);
        // Check if the opener element is inside an AJAX container.
        const $element = $(event.target);
        const ajaxContainer = $element.data('uiDialog')
          ? $element
              .data('uiDialog')
              .opener.closest('[data-drupal-ajax-container]')
          : [];

        // If the opener element was in an ajax container, and focus is on the
        // body element, we can assume focus was lost. To recover, focus is
        // moved to the first focusable element in the container.
        if (
          ajaxContainer.length &&
          (document.activeElement === document.body ||
            $(document.activeElement).not(':visible'))
        ) {
          const focusableChildren = focusable(ajaxContainer[0]);
          if (focusableChildren.length > 0) {
            setTimeout(() => {
              focusableChildren[0].focus();
            }, 0);
          }
        }
        $(event.target).remove();
      };
    },

    /**
     * Scan a dialog for any primary buttons and move them to the button area.
     *
     * @param {jQuery} $dialog
     *   A jQuery object containing the element that is the dialog target.
     *
     * @return {Array}
     *   An array of buttons that need to be added to the button area.
     */
    prepareDialogButtons($dialog) {
      const buttons = [];
      const buttonSelectors =
        '.form-actions input[type=submit], .form-actions a.button, .form-actions a.action-link';
      const buttonElements = $dialog[0].querySelectorAll(buttonSelectors);

      buttonElements.forEach((button) => {
        button.style.display = 'none';
        buttons.push({
          text: button.innerHTML || button.getAttribute('value'),
          class: button.getAttribute('class'),
          'data-once': button.dataset.once,
          click(e) {
            if (button.tagName === 'A') {
              button.click();
            } else {
              ['mousedown', 'mouseup', 'click'].forEach((event) =>
                button.dispatchEvent(new MouseEvent(event)),
              );
            }
            e.preventDefault();
          },
        });
      });
      return buttons;
    },
  };

  /**
   * Command to open a dialog.
   *
   * @param {Drupal.Ajax} ajax
   *   The Drupal Ajax object.
   * @param {object} response
   *   Object holding the server response.
   * @param {number} [status]
   *   The HTTP status code.
   *
   * @return {boolean|undefined}
   *   Returns false if there was no selector property in the response object.
   */
  Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) {
    if (!response.selector) {
      return false;
    }
    let dialog = document.querySelector(response.selector);
    if (!dialog) {
      // Create the element if needed.
      dialog = document.createElement('div');
      dialog.id = response.selector.replace(/^#/, '');
      dialog.classList.add('ui-front');
      document.body.appendChild(dialog);
    }
    // Set up the wrapper, if there isn't one.
    if (!ajax.wrapper) {
      ajax.wrapper = dialog.id;
    }

    // Use the ajax.js insert command to populate the dialog contents.
    response.command = 'insert';
    response.method = 'html';
    ajax.commands.insert(ajax, response, status);

    // Move the buttons to the jQuery UI dialog buttons area.
    response.dialogOptions = response.dialogOptions || {};
    if (typeof response.dialogOptions.drupalAutoButtons === 'undefined') {
      response.dialogOptions.drupalAutoButtons = true;
    } else if (response.dialogOptions.drupalAutoButtons === 'false') {
      response.dialogOptions.drupalAutoButtons = false;
    } else {
      response.dialogOptions.drupalAutoButtons =
        !!response.dialogOptions.drupalAutoButtons;
    }
    if (
      !response.dialogOptions.buttons &&
      response.dialogOptions.drupalAutoButtons
    ) {
      response.dialogOptions.buttons =
        Drupal.behaviors.dialog.prepareDialogButtons($(dialog));
    }

    const dialogButtonsChange = () => {
      const buttons = Drupal.behaviors.dialog.prepareDialogButtons($(dialog));
      $(dialog).dialog('option', 'buttons', buttons);
    };

    // Bind dialogButtonsChange.
    dialog.addEventListener('dialogButtonsChange', dialogButtonsChange);
    dialog.addEventListener('dialog:beforeclose', (event) => {
      dialog.removeEventListener('dialogButtonsChange', dialogButtonsChange);
    });

    // Open the dialog itself.
    const createdDialog = Drupal.dialog(dialog, response.dialogOptions);
    if (response.dialogOptions.modal) {
      createdDialog.showModal();
    } else {
      createdDialog.show();
    }

    // Add the standard Drupal class for buttons for style consistency.
    dialog.parentElement
      ?.querySelector('.ui-dialog-buttonset')
      ?.classList.add('form-actions');
  };

  /**
   * Command to close a dialog.
   *
   * If no selector is given, it defaults to trying to close the modal.
   *
   * @param {Drupal.Ajax} [ajax]
   *   The ajax object.
   * @param {object} response
   *   Object holding the server response.
   * @param {string} response.selector
   *   The selector of the dialog.
   * @param {boolean} response.persist
   *   Whether to persist the dialog element or not.
   * @param {number} [status]
   *   The HTTP status code.
   */
  Drupal.AjaxCommands.prototype.closeDialog = function (
    ajax,
    response,
    status,
  ) {
    const dialog = document.querySelector(response.selector);
    if (dialog) {
      Drupal.dialog(dialog).close();
      if (!response.persist) {
        dialog.remove();
      }
    }
  };

  /**
   * Command to set a dialog property.
   *
   * JQuery UI specific way of setting dialog options.
   *
   * @param {Drupal.Ajax} [ajax]
   *   The Drupal Ajax object.
   * @param {object} response
   *   Object holding the server response.
   * @param {string} response.selector
   *   Selector for the dialog element.
   * @param {string} response.optionsName
   *   Name of a key to set.
   * @param {string} response.optionValue
   *   Value to set.
   * @param {number} [status]
   *   The HTTP status code.
   */
  Drupal.AjaxCommands.prototype.setDialogOption = function (
    ajax,
    response,
    status,
  ) {
    const dialog = document.querySelector(response.selector);
    if (dialog) {
      $(dialog).dialog('option', response.optionName, response.optionValue);
    }
  };

  /**
   * Binds a listener on dialog creation to handle the cancel link.
   *
   * @param {DrupalDialogEvent} e
   *   The event triggered.
   * @param {Drupal.dialog~dialogDefinition} dialog
   *   The dialog instance.
   * @param {object} [settings]
   *   Dialog settings.
   */
  window.addEventListener('dialog:aftercreate', (event) => {
    const dialog = event.dialog;
    const cancelButton = event.target.querySelector('.dialog-cancel');
    const cancelClick = (e) => {
      dialog.close('cancel');
      e.preventDefault();
      e.stopPropagation();
    };
    cancelButton?.removeEventListener('click', cancelClick);
    cancelButton?.addEventListener('click', cancelClick);
  });

  /**
   * Ajax command to open URL in a modal dialog.
   *
   * @param {Drupal.Ajax} [ajax]
   *   An Ajax object.
   * @param {object} response
   *   The Ajax response.
   */
  Drupal.AjaxCommands.prototype.openModalDialogWithUrl = function (
    ajax,
    response,
  ) {
    const dialogOptions = response.dialogOptions || {};
    const elementSettings = {
      progress: { type: 'throbber' },
      dialogType: 'modal',
      dialog: dialogOptions,
      url: response.url,
      httpMethod: 'GET',
    };
    Drupal.ajax(elementSettings).execute();
  };
})(jQuery, Drupal, window.tabbable);