Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Toggling the visibility of the secondary navigation on mobile.
18
 *
19
 * @module     theme_universe/drawers
20
 * @copyright  2021 Bas Brands
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
define(function (require) {
25
  "use strict";
26
 
27
  const ModalBackdrop = require("core/modal_backdrop");
28
  const Templates = require("core/templates");
29
  const Aria = require("core/aria");
30
  const { dispatchEvent } = require("core/event_dispatcher");
31
  const { debounce } = require("core/utils");
32
  const { isSmall, isLarge } = require("core/pagehelpers");
33
  const Pending = require("core/pending");
34
  const UserRepository = require("core_user/repository");
35
  // The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.
36
  const jQuery = require("jquery");
37
 
38
  let backdropPromise = null;
39
 
40
  const drawerMap = new Map();
41
 
42
  const SELECTORS = {
43
    BUTTONS: '[data-toggler="drawers"]',
44
    CLOSEBTN: '[data-toggler="drawers"][data-action="closedrawer"]',
45
    OPENBTN: '[data-toggler="drawers"][data-action="opendrawer"]',
46
    TOGGLEBTN: '[data-toggler="drawers"][data-action="toggle"]',
47
    DRAWERS: '[data-region="fixed-drawer"]',
48
    DRAWERCONTENT: ".drawercontent",
49
    PAGECONTENT: "#page-content",
50
    HEADERCONTENT: ".drawerheadercontent",
51
  };
52
 
53
  const CLASSES = {
54
    SCROLLED: "scrolled",
55
    SHOW: "show",
56
    NOTINITIALISED: "not-initialized",
57
  };
58
 
59
  /**
60
   * Pixel thresshold to auto-hide drawers.
61
   *
62
   * @type {Number}
63
   */
64
  const THRESHOLD = 20;
65
 
66
  /**
67
   * Try to get the drawer z-index from the page content.
68
   *
69
   * @returns {Number|null} the z-index of the drawer.
70
   * @private
71
   */
72
  const getDrawerZIndex = () => {
73
    const drawer = document.querySelector(SELECTORS.DRAWERS);
74
    if (!drawer) {
75
      return null;
76
    }
77
    return parseInt(window.getComputedStyle(drawer).zIndex, 10);
78
  };
79
 
80
  /**
81
   * Add a backdrop to the page.
82
   *
83
   * @returns {Promise} rendering of modal backdrop.
84
   * @private
85
   */
86
  const getBackdrop = () => {
87
    if (!backdropPromise) {
88
      backdropPromise = Templates.render("core/modal_backdrop", {})
89
        .then((html) => new ModalBackdrop(html))
90
        .then((modalBackdrop) => {
91
          const drawerZindex = getDrawerZIndex();
92
          if (drawerZindex) {
93
            modalBackdrop.setZIndex(getDrawerZIndex() - 1);
94
          }
95
          modalBackdrop
96
            .getAttachmentPoint()
97
            .get(0)
98
            .addEventListener("click", (e) => {
99
              e.preventDefault();
100
              Drawers.closeAllDrawers();
101
            });
102
          return modalBackdrop;
103
        })
104
        .catch();
105
    }
106
    return backdropPromise;
107
  };
108
 
109
  /**
110
   * Get the button element to open a specific drawer.
111
   *
112
   * @param {String} drawerId the drawer element Id
113
   * @return {HTMLElement|undefined} the open button element
114
   * @private
115
   */
116
  const getDrawerOpenButton = (drawerId) => {
117
    let openButton = document.querySelector(
118
      `${SELECTORS.OPENBTN}[data-target="${drawerId}"]`
119
    );
120
    if (!openButton) {
121
      openButton = document.querySelector(
122
        `${SELECTORS.TOGGLEBTN}[data-target="${drawerId}"]`
123
      );
124
    }
125
    return openButton;
126
  };
127
 
128
  /**
129
   * Disable drawer tooltips.
130
   *
131
   * @param {HTMLElement} drawerNode the drawer main node
132
   * @private
133
   */
134
  const disableDrawerTooltips = (drawerNode) => {
135
    const buttons = [
136
      drawerNode.querySelector(SELECTORS.CLOSEBTN),
137
      getDrawerOpenButton(drawerNode.id),
138
    ];
139
    buttons.forEach((button) => {
140
      if (!button) {
141
        return;
142
      }
143
      disableButtonTooltip(button);
144
    });
145
  };
146
 
147
  /**
148
   * Disable the button tooltips.
149
   *
150
   * @param {HTMLElement} button the button element
151
   * @param {boolean} enableOnBlur if the tooltip must be re-enabled on blur.
152
   * @private
153
   */
154
  const disableButtonTooltip = (button, enableOnBlur) => {
155
    if (button.hasAttribute("data-original-title")) {
156
      // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.
157
      jQuery(button).tooltip("disable");
158
      button.setAttribute("title", button.dataset.originalTitle);
159
    } else {
160
      button.dataset.disabledToggle = button.dataset.toggle;
161
      button.removeAttribute("data-toggle");
162
    }
163
    if (enableOnBlur) {
164
      button.dataset.restoreTooltipOnBlur = true;
165
    }
166
  };
167
 
168
  /**
169
   * Enable drawer tooltips.
170
   *
171
   * @param {HTMLElement} drawerNode the drawer main node
172
   * @private
173
   */
174
  const enableDrawerTooltips = (drawerNode) => {
175
    const buttons = [
176
      drawerNode.querySelector(SELECTORS.CLOSEBTN),
177
      getDrawerOpenButton(drawerNode.id),
178
    ];
179
    buttons.forEach((button) => {
180
      if (!button) {
181
        return;
182
      }
183
      enableButtonTooltip(button);
184
    });
185
  };
186
 
187
  /**
188
   * Enable the button tooltips.
189
   *
190
   * @param {HTMLElement} button the button element
191
   * @private
192
   */
193
  const enableButtonTooltip = (button) => {
194
    // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.
195
    if (button.hasAttribute("data-original-title")) {
196
      jQuery(button).tooltip("enable");
197
      button.removeAttribute("title");
198
    } else if (button.dataset.disabledToggle) {
199
      button.dataset.toggle = button.dataset.disabledToggle;
200
      jQuery(button).tooltip();
201
    }
202
    delete button.dataset.restoreTooltipOnBlur;
203
  };
204
 
205
  /**
206
   * Add scroll listeners to a drawer element.
207
   *
208
   * @param {HTMLElement} drawerNode the drawer main node
209
   * @private
210
   */
211
  const addInnerScrollListener = (drawerNode) => {
212
    const content = drawerNode.querySelector(SELECTORS.DRAWERCONTENT);
213
    if (!content) {
214
      return;
215
    }
216
    content.addEventListener("scroll", () => {
217
      drawerNode.classList.toggle(CLASSES.SCROLLED, content.scrollTop != 0);
218
    });
219
  };
220
 
221
  /**
222
   * The Drawers class is used to control on-screen drawer elements.
223
   *
224
   * It handles opening, and closing of drawer elements, as well as more detailed behaviours such as closing a drawer when
225
   * another drawer is opened, and supports closing a drawer when the screen is resized.
226
   *
227
   * Drawers are instantiated on page load, and can also be toggled lazily when toggling any drawer toggle, open button,
228
   * or close button.
229
   *
230
   * A range of show and hide events are also dispatched as detailed in the class
231
   * {@link module:theme_universe/drawers#eventTypes eventTypes} object.
232
   *
233
   * @example <caption>Standard usage</caption>
234
   *
235
   * // The module just needs to be included to add drawer support.
236
   * import 'theme_universe/drawers';
237
   *
238
   * @example <caption>Manually open or close any drawer</caption>
239
   *
240
   * import Drawers from 'theme_universe/drawers';
241
   *
242
   * const myDrawer = Drawers.getDrawerInstanceForNode(document.querySelector('.myDrawerNode');
243
   * myDrawer.closeDrawer();
244
   *
245
   * @example <caption>Listen to the before show event and cancel it</caption>
246
   *
247
   * import Drawers from 'theme_universe/drawers';
248
   *
249
   * document.addEventListener(Drawers.eventTypes.drawerShow, e => {
250
   *     // The drawer which will be shown.
251
   *     window.console.log(e.target);
252
   *
253
   *     // The instance of the Drawers class for this drawer.
254
   *     window.console.log(e.detail.drawerInstance);
255
   *
256
   *     // Prevent this drawer from being shown.
257
   *     e.preventDefault();
258
   * });
259
   *
260
   * @example <caption>Listen to the shown event</caption>
261
   *
262
   * document.addEventListener(Drawers.eventTypes.drawerShown, e => {
263
   *     // The drawer which was shown.
264
   *     window.console.log(e.target);
265
   *
266
   *     // The instance of the Drawers class for this drawer.
267
   *     window.console.log(e.detail.drawerInstance);
268
   * });
269
   */
270
  class Drawers {
271
    /**
272
     * The underlying HTMLElement which is controlled.
273
     */
274
    drawerNode = null;
275
 
276
    /**
277
     * The drawer page bounding box dimensions.
278
     * @var {DOMRect} boundingRect
279
     */
280
    boundingRect = null;
281
 
282
    constructor(drawerNode) {
283
      // Some behat tests may use fake drawer divs to test components in drawers.
284
      if (drawerNode.dataset.behatFakeDrawer !== undefined) {
285
        return;
286
      }
287
 
288
      this.drawerNode = drawerNode;
289
 
290
      if (isSmall()) {
291
        this.closeDrawer({
292
          focusOnOpenButton: false,
293
          updatePreferences: false,
294
        });
295
      }
296
 
297
      if (this.drawerNode.classList.contains(CLASSES.SHOW)) {
298
        this.openDrawer({ focusOnCloseButton: false });
299
      } else if (this.drawerNode.dataset.forceopen == 1) {
300
        if (!isSmall()) {
301
          this.openDrawer({ focusOnCloseButton: false });
302
        }
303
      } else {
304
        Aria.hide(this.drawerNode);
305
      }
306
 
307
      // Disable tooltips in small screens.
308
      if (isSmall()) {
309
        disableDrawerTooltips(this.drawerNode);
310
      }
311
 
312
      addInnerScrollListener(this.drawerNode);
313
 
314
      drawerMap.set(drawerNode, this);
315
 
316
      drawerNode.classList.remove(CLASSES.NOTINITIALISED);
317
    }
318
 
319
    /**
320
     * Whether the drawer is open.
321
     *
322
     * @returns {boolean}
323
     */
324
    get isOpen() {
325
      return this.drawerNode.classList.contains(CLASSES.SHOW);
326
    }
327
 
328
    /**
329
     * Whether the drawer should close when the window is resized
330
     *
331
     * @returns {boolean}
332
     */
333
    get closeOnResize() {
334
      return !!parseInt(this.drawerNode.dataset.closeOnResize);
335
    }
336
 
337
    /**
338
     * The list of event types.
339
     *
340
     * @static
341
     * @property {String} drawerShow See {@link event:theme_universe/drawers:show}
342
     * @property {String} drawerShown See {@link event:theme_universe/drawers:shown}
343
     * @property {String} drawerHide See {@link event:theme_universe/drawers:hide}
344
     * @property {String} drawerHidden See {@link event:theme_universe/drawers:hidden}
345
     */
346
    static eventTypes = {
347
      /**
348
       * An event triggered before a drawer is shown.
349
       *
350
       * @event theme_universe/drawers:show
351
       * @type {CustomEvent}
352
       * @property {HTMLElement} target The drawer that will be opened.
353
       */
354
      drawerShow: "theme_universe/drawers:show",
355
 
356
      /**
357
       * An event triggered after a drawer is shown.
358
       *
359
       * @event theme_universe/drawers:shown
360
       * @type {CustomEvent}
361
       * @property {HTMLElement} target The drawer that was be opened.
362
       */
363
      drawerShown: "theme_universe/drawers:shown",
364
 
365
      /**
366
       * An event triggered before a drawer is hidden.
367
       *
368
       * @event theme_universe/drawers:hide
369
       * @type {CustomEvent}
370
       * @property {HTMLElement} target The drawer that will be hidden.
371
       */
372
      drawerHide: "theme_universe/drawers:hide",
373
 
374
      /**
375
       * An event triggered after a drawer is hidden.
376
       *
377
       * @event theme_universe/drawers:hidden
378
       * @type {CustomEvent}
379
       * @property {HTMLElement} target The drawer that was be hidden.
380
       */
381
      drawerHidden: "theme_universe/drawers:hidden",
382
    };
383
 
384
    /**
385
     * Get the drawer instance for the specified node
386
     *
387
     * @param {HTMLElement} drawerNode
388
     * @returns {module:theme_universe/drawers}
389
     */
390
    static getDrawerInstanceForNode(drawerNode) {
391
      if (!drawerMap.has(drawerNode)) {
392
        new Drawers(drawerNode);
393
      }
394
 
395
      return drawerMap.get(drawerNode);
396
    }
397
 
398
    /**
399
     * Dispatch a drawer event.
400
     *
401
     * @param {string} eventname the event name
402
     * @param {boolean} cancelable if the event is cancelable
403
     * @returns {CustomEvent} the resulting custom event
404
     */
405
    dispatchEvent(eventname, cancelable = false) {
406
      return dispatchEvent(
407
        eventname,
408
        {
409
          drawerInstance: this,
410
        },
411
        this.drawerNode,
412
        {
413
          cancelable,
414
        }
415
      );
416
    }
417
 
418
    /**
419
     * Open the drawer.
420
     *
421
     * By default, openDrawer sets the page focus to the close drawer button. However, when a drawer is open at page
422
     * load, this represents an accessibility problem as the initial focus changes without any user interaction. The
423
     * focusOnCloseButton parameter can be set to false to prevent this behaviour.
424
     *
425
     * @param {object} args
426
     * @param {boolean} [args.focusOnCloseButton=true] Whether to alter page focus when opening the drawer
427
     */
428
    openDrawer({ focusOnCloseButton = true } = {}) {
429
      const pendingPromise = new Pending("theme_universe/drawers:open");
430
      const showEvent = this.dispatchEvent(Drawers.eventTypes.drawerShow, true);
431
      if (showEvent.defaultPrevented) {
432
        return;
433
      }
434
 
435
      // Hide close button and header content while the drawer is showing to prevent glitchy effects.
436
      this.drawerNode
437
        .querySelector(SELECTORS.CLOSEBTN)
438
        ?.classList.toggle("hidden", true);
439
      this.drawerNode
440
        .querySelector(SELECTORS.HEADERCONTENT)
441
        ?.classList.toggle("hidden", true);
442
 
443
      // Remove open tooltip if still visible.
444
      let openButton = getDrawerOpenButton(this.drawerNode.id);
445
      if (openButton && openButton.hasAttribute("data-original-title")) {
446
        // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.
447
        jQuery(openButton)?.tooltip("hide");
448
      }
449
 
450
      Aria.unhide(this.drawerNode);
451
      this.drawerNode.classList.add(CLASSES.SHOW);
452
 
453
      const preference = this.drawerNode.dataset.preference;
454
      if (preference && !isSmall() && this.drawerNode.dataset.forceopen != 1) {
455
        UserRepository.setUserPreference(preference, true);
456
      }
457
 
458
      const state = this.drawerNode.dataset.state;
459
      if (state) {
460
        const page = document.getElementById("page");
461
        page.classList.add(state);
462
      }
463
 
464
      this.boundingRect = this.drawerNode.getBoundingClientRect();
465
 
466
      if (isSmall()) {
467
        getBackdrop()
468
          .then((backdrop) => {
469
            backdrop.show();
470
 
471
            const pageWrapper = document.getElementById("page");
472
            pageWrapper.style.overflow = "hidden";
473
            return backdrop;
474
          })
475
          .catch();
476
      }
477
 
478
      // Show close button and header content once the drawer is fully opened.
479
      const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN);
480
      const headerContent = this.drawerNode.querySelector(
481
        SELECTORS.HEADERCONTENT
482
      );
483
      if (focusOnCloseButton && closeButton) {
484
        disableButtonTooltip(closeButton, true);
485
      }
486
      setTimeout(() => {
487
        closeButton.classList.toggle("hidden", false);
488
        headerContent.classList.toggle("hidden", false);
489
        if (focusOnCloseButton) {
490
          closeButton.focus();
491
        }
492
        pendingPromise.resolve();
493
      }, 300);
494
 
495
      this.dispatchEvent(Drawers.eventTypes.drawerShown);
496
    }
497
 
498
    /**
499
     * Close the drawer.
500
     *
501
     * @param {object} args
502
     * @param {boolean} [args.focusOnOpenButton=true] Whether to alter page focus when opening the drawer
503
     * @param {boolean} [args.updatePreferences=true] Whether to update the user prewference
504
     */
505
    closeDrawer({ focusOnOpenButton = true, updatePreferences = true } = {}) {
506
      const pendingPromise = new Pending("theme_universe/drawers:close");
507
 
508
      const hideEvent = this.dispatchEvent(Drawers.eventTypes.drawerHide, true);
509
      if (hideEvent.defaultPrevented) {
510
        return;
511
      }
512
 
513
      // Hide close button and header content while the drawer is hiding to prevent glitchy effects.
514
      const closeButton = this.drawerNode.querySelector(SELECTORS.CLOSEBTN);
515
      closeButton?.classList.toggle("hidden", true);
516
      const headerContent = this.drawerNode.querySelector(
517
        SELECTORS.HEADERCONTENT
518
      );
519
      headerContent?.classList.toggle("hidden", true);
520
      // Remove the close button tooltip if visible.
521
      if (closeButton.hasAttribute("data-original-title")) {
522
        // The jQuery is still used in Boostrap 4. It can we removed when MDL-71979 is integrated.
523
        jQuery(closeButton)?.tooltip("hide");
524
      }
525
 
526
      const preference = this.drawerNode.dataset.preference;
527
      if (preference && updatePreferences && !isSmall()) {
528
        UserRepository.setUserPreference(preference, false);
529
      }
530
 
531
      const state = this.drawerNode.dataset.state;
532
      if (state) {
533
        const page = document.getElementById("page");
534
        page.classList.remove(state);
535
      }
536
 
537
      Aria.hide(this.drawerNode);
538
      this.drawerNode.classList.remove(CLASSES.SHOW);
539
 
540
      getBackdrop()
541
        .then((backdrop) => {
542
          backdrop.hide();
543
 
544
          if (isSmall()) {
545
            const pageWrapper = document.getElementById("page");
546
            pageWrapper.style.overflow = "visible";
547
          }
548
          return backdrop;
549
        })
550
        .catch();
551
 
552
      // Move focus to the open drawer (or toggler) button once the drawer is hidden.
553
      let openButton = getDrawerOpenButton(this.drawerNode.id);
554
      if (openButton) {
555
        disableButtonTooltip(openButton, true);
556
      }
557
      setTimeout(() => {
558
        if (openButton && focusOnOpenButton) {
559
          openButton.focus();
560
        }
561
        pendingPromise.resolve();
562
      }, 300);
563
 
564
      this.dispatchEvent(Drawers.eventTypes.drawerHidden);
565
    }
566
 
567
    /**
568
     * Toggle visibility of the drawer.
569
     */
570
    toggleVisibility() {
571
      if (this.drawerNode.classList.contains(CLASSES.SHOW)) {
572
        this.closeDrawer();
573
      } else {
574
        this.openDrawer();
575
      }
576
    }
577
 
578
    /**
579
     * Displaces the drawer outsite the page.
580
     *
581
     * @param {Number} scrollPosition the page current scroll position
582
     */
583
    displace(scrollPosition) {
584
      let displace = scrollPosition;
585
      let openButton = getDrawerOpenButton(this.drawerNode.id);
586
      if (scrollPosition === 0) {
587
        this.drawerNode.style.transform = "";
588
        if (openButton) {
589
          openButton.style.transform = "";
590
        }
591
        return;
592
      }
593
      const state = this.drawerNode.dataset?.state;
594
      const drawrWidth = this.drawerNode.offsetWidth;
595
      let scrollThreshold = drawrWidth;
596
      let direction = -1;
597
      if (state === "show-drawer-right") {
598
        direction = 1;
599
        scrollThreshold = THRESHOLD;
600
      }
601
      // LTR scroll is positive while RTL scroll is negative.
602
      if (Math.abs(scrollPosition) > scrollThreshold) {
603
        displace = Math.sign(scrollPosition) * (drawrWidth + THRESHOLD);
604
      }
605
      displace *= direction;
606
      const transform = `translateX(${displace}px)`;
607
      if (openButton) {
608
        openButton.style.transform = transform;
609
      }
610
      this.drawerNode.style.transform = transform;
611
    }
612
 
613
    /**
614
     * Prevent drawer from overlapping an element.
615
     *
616
     * @param {HTMLElement} currentFocus
617
     */
618
    preventOverlap(currentFocus) {
619
      // Start position drawer (aka. left drawer) will never overlap with the page content.
620
      if (
621
        !this.isOpen ||
622
        this.drawerNode.dataset?.state === "show-drawer-left"
623
      ) {
624
        return;
625
      }
626
      const drawrWidth = this.drawerNode.offsetWidth;
627
      const element = currentFocus.getBoundingClientRect();
628
 
629
      // The this.boundingRect is calculated only once and it is reliable
630
      // for horizontal overlapping (which is the most common). However,
631
      // it is not reliable for vertical overlapping because the drawer
632
      // height can be changed by other elements like sticky footer.
633
      // To prevent recalculating the boundingRect on every
634
      // focusin event, we use horizontal overlapping as first fast check.
635
      let overlapping =
636
        element.right + THRESHOLD > this.boundingRect.left &&
637
        element.left - THRESHOLD < this.boundingRect.right;
638
      if (overlapping) {
639
        const currentBoundingRect = this.drawerNode.getBoundingClientRect();
640
        overlapping =
641
          element.bottom > currentBoundingRect.top &&
642
          element.top < currentBoundingRect.bottom;
643
      }
644
 
645
      if (overlapping) {
646
        // Force drawer to displace out of the page.
647
        let displaceOut = drawrWidth + 1;
648
        if (window.right_to_left()) {
649
          displaceOut *= -1;
650
        }
651
        this.displace(displaceOut);
652
      } else {
653
        // Reset drawer displacement.
654
        this.displace(window.scrollX);
655
      }
656
    }
657
 
658
    /**
659
     * Close all drawers.
660
     */
661
    static closeAllDrawers() {
662
      drawerMap.forEach((drawerInstance) => {
663
        drawerInstance.closeDrawer();
664
      });
665
    }
666
 
667
    /**
668
     * Close all drawers except for the specified drawer.
669
     *
670
     * @param {module:theme_universe/drawers} comparisonInstance
671
     */
672
    static closeOtherDrawers(comparisonInstance) {
673
      drawerMap.forEach((drawerInstance) => {
674
        if (drawerInstance === comparisonInstance) {
675
          return;
676
        }
677
 
678
        drawerInstance.closeDrawer();
679
      });
680
    }
681
 
682
    /**
683
     * Prevent drawers from covering the focused element.
684
     */
685
    static preventCoveringFocusedElement() {
686
      const currentFocus = document.activeElement;
687
      // Focus on page layout elements should be ignored.
688
      const pagecontent = document.querySelector(SELECTORS.PAGECONTENT);
689
      if (!currentFocus || !pagecontent?.contains(currentFocus)) {
690
        Drawers.displaceDrawers(window.scrollX);
691
        return;
692
      }
693
      drawerMap.forEach((drawerInstance) => {
694
        drawerInstance.preventOverlap(currentFocus);
695
      });
696
    }
697
 
698
    /**
699
     * Prevent drawer from covering the content when the page content covers the full page.
700
     *
701
     * @param {Number} displace
702
     */
703
    static displaceDrawers(displace) {
704
      drawerMap.forEach((drawerInstance) => {
705
        drawerInstance.displace(displace);
706
      });
707
    }
708
  }
709
 
710
  /**
711
   * Set the last used attribute for the last used toggle button for a drawer.
712
   *
713
   * @param {object} toggleButton The clicked button.
714
   */
715
  const setLastUsedToggle = (toggleButton) => {
716
    if (toggleButton.dataset.target) {
717
      document
718
        .querySelectorAll(
719
          `${SELECTORS.BUTTONS}[data-target="${toggleButton.dataset.target}"]`
720
        )
721
        .forEach((btn) => {
722
          btn.dataset.lastused = false;
723
        });
724
      toggleButton.dataset.lastused = true;
725
    }
726
  };
727
 
728
  /**
729
   * Set the focus to the last used button to open this drawer.
730
   * @param {string} target The drawer target.
731
   */
732
  const focusLastUsedToggle = (target) => {
733
    const lastUsedButton = document.querySelector(
734
      `${SELECTORS.BUTTONS}[data-target="${target}"][data-lastused="true"`
735
    );
736
    if (lastUsedButton) {
737
      lastUsedButton.focus();
738
    }
739
  };
740
 
741
  /**
742
   * Register the event listeners for the drawer.
743
   *
744
   * @private
745
   */
746
  const registerListeners = () => {
747
    // Listen for show/hide events.
748
    document.addEventListener("click", (e) => {
749
      const toggleButton = e.target.closest(SELECTORS.TOGGLEBTN);
750
      if (toggleButton && toggleButton.dataset.target) {
751
        e.preventDefault();
752
        const targetDrawer = document.getElementById(
753
          toggleButton.dataset.target
754
        );
755
        const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);
756
        setLastUsedToggle(toggleButton);
757
 
758
        drawerInstance.toggleVisibility();
759
      }
760
 
761
      const openDrawerButton = e.target.closest(SELECTORS.OPENBTN);
762
      if (openDrawerButton && openDrawerButton.dataset.target) {
763
        e.preventDefault();
764
        const targetDrawer = document.getElementById(
765
          openDrawerButton.dataset.target
766
        );
767
        const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);
768
        setLastUsedToggle(toggleButton);
769
 
770
        drawerInstance.openDrawer();
771
      }
772
 
773
      const closeDrawerButton = e.target.closest(SELECTORS.CLOSEBTN);
774
      if (closeDrawerButton && closeDrawerButton.dataset.target) {
775
        e.preventDefault();
776
        const targetDrawer = document.getElementById(
777
          closeDrawerButton.dataset.target
778
        );
779
        const drawerInstance = Drawers.getDrawerInstanceForNode(targetDrawer);
780
 
781
        drawerInstance.closeDrawer();
782
        focusLastUsedToggle(closeDrawerButton.dataset.target);
783
      }
784
    });
785
 
786
    // Close drawer when another drawer opens.
787
    document.addEventListener(Drawers.eventTypes.drawerShow, (e) => {
788
      if (isLarge()) {
789
        return;
790
      }
791
      Drawers.closeOtherDrawers(e.detail.drawerInstance);
792
    });
793
 
794
    // Tooglers and openers blur listeners.
795
    const btnSelector = `${SELECTORS.TOGGLEBTN}, ${SELECTORS.OPENBTN}, ${SELECTORS.CLOSEBTN}`;
796
    document.addEventListener("focusout", (e) => {
797
      const button = e.target.closest(btnSelector);
798
      if (button?.dataset.restoreTooltipOnBlur !== undefined) {
799
        enableButtonTooltip(button);
800
      }
801
    });
802
 
803
    const closeOnResizeListener = () => {
804
      if (isSmall()) {
805
        let anyOpen = false;
806
        drawerMap.forEach((drawerInstance) => {
807
          disableDrawerTooltips(drawerInstance.drawerNode);
808
          if (drawerInstance.isOpen) {
809
            if (drawerInstance.closeOnResize) {
810
              drawerInstance.closeDrawer();
811
            } else {
812
              anyOpen = true;
813
            }
814
          }
815
        });
816
 
817
        if (anyOpen) {
818
          getBackdrop()
819
            .then((backdrop) => backdrop.show())
820
            .catch();
821
        }
822
      } else {
823
        drawerMap.forEach((drawerInstance) => {
824
          enableDrawerTooltips(drawerInstance.drawerNode);
825
        });
826
        getBackdrop()
827
          .then((backdrop) => backdrop.hide())
828
          .catch();
829
      }
830
    };
831
 
832
    document.addEventListener("scroll", () => {
833
      const body = document.querySelector("body");
834
      if (window.scrollY >= window.innerHeight) {
835
        body.classList.add(CLASSES.SCROLLED);
836
      } else {
837
        body.classList.remove(CLASSES.SCROLLED);
838
      }
839
      // Horizontal scroll listener to displace the drawers to prevent covering
840
      // any possible sticky content.
841
      Drawers.displaceDrawers(window.scrollX);
842
    });
843
 
844
    const preventOverlap = debounce(Drawers.preventCoveringFocusedElement, 100);
845
    document.addEventListener("focusin", preventOverlap);
846
    document.addEventListener("focusout", preventOverlap);
847
 
848
    window.addEventListener("resize", debounce(closeOnResizeListener, 400));
849
  };
850
 
851
  registerListeners();
852
 
853
  const drawers = document.querySelectorAll(SELECTORS.DRAWERS);
854
  drawers.forEach((drawerNode) => Drawers.getDrawerInstanceForNode(drawerNode));
855
 
856
  return Drawers;
857
});