web-dev-qa-db-fra.com

Comment ajouter un seul menu déroulant au corps dans Bootstrap

J'ai vu la documentation du menu déroulant en tant que composant et séparément à l'aide de javascript .

Je me demande s'il est possible d'ajouter un seul menu déroulant dans le corps du site Web (position absolue par rapport à l'élément de bouton cliquable).

Pourquoi?  

  • Parce que si j'ai une table avec 500 lignes, je ne veux pas ajouter 500 fois la même liste de 10 éléments, ce qui rend le HTML résultant plus grand et plus lent quand il s'agit de JS.

  • Parce que l'élément parent peut être masqué, mais je souhaite tout de même que le menu déroulant soit visible jusqu'à ce que l'utilisateur clique dessus, ce qui le déconcentre.

J'ai trouvé plus de personnes demandant cette fonctionnalité mais je n'ai rien trouvé dans la documentation à ce sujet.

11
Alvaro

Comme le disent les documents d'amorçage , il n'y a pas d'options pour les menus déroulants ... C'est triste, mais cela signifie qu'il n'y a actuellement pas de solution de «démarrage» pour la fonctionnalité souhaitée. Cependant, il existe maintenant une solution dans Angular-UI/Bootstrap kit si vous l’utilisez. Le ticket que vous avez référencé est fermé car il était finalement ajouté à l'interface utilisateur angulaire au 15 juillet 2015. 

Tout ce que vous avez à faire est d'ajouter 'add dropdown-append-to-body à l'élément dropdown à ajouter au menu déroulant interne du corps. Ceci est utile lorsque le bouton déroulant est dans une div avec débordement: hidden, sinon le menu serait masqué. '(RÉFÉRENCE)

<div class="btn-group" dropdown dropdown-append-to-body>
  <button type="button" class="btn btn-primary dropdown-toggle" dropdown-toggle>Dropdown on Body <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu">
    <li><a href="#">Action</a></li>
    <li><a href="#">Another action</a></li>
    <li><a href="#">Something else here</a></li>
    <li class="divider"></li>
    <li><a href="#">Separated link</a></li>
  </ul>
</div>

J'espère que cela t'aides!


EDIT

Dans un effort pour répondre à une autre SO question, j'ai trouvé une solution qui fonctionne plutôt bien si vous n'utilisiez pas Angular-UI. C'est peut-être un «hacky», mais cela ne rompt pas la fonctionnalité du menu de démarrage, et il semble bien fonctionner avec la plupart des cas d'utilisation pour lesquels je l'ai utilisé.

Je vais donc laisser quelques violons au cas où quelqu'un verrait cela et serait intéressé. La première illustre pourquoi l'utilisation d'un menu ajouté au corps pourrait être Nice, la seconde montre la solution de travail:

Problème FIDDLE

Le problème: une liste déroulante de sélection dans un corps de panneau

<div class="panel panel-default">
  <div class="panel-body">
    <div class="btn-group">
      <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
        <span data-bind="label">Select One</span>&nbsp;<span class="caret"></span>
      </button>
      <ul class="dropdown-menu" role="menu">
        <li><a href="#">Item 1</a></li>
        <li><a href="#">Another item</a></li>
        <li><a href="#">This is a longer item that will not fit properly</a></li>
      </ul>
    </div>
  </div>
</div>

Solution FIDDLE

(function () {
    // hold onto the drop down menu                                             
    var dropdownMenu;

    // and when you show it, move it to the body                                     
    $(window).on('show.bs.dropdown', function (e) {

        // grab the menu        
        dropdownMenu = $(e.target).find('.dropdown-menu');

        // detach it and append it to the body
        $('body').append(dropdownMenu.detach());

        // grab the new offset position
        var eOffset = $(e.target).offset();

        // make sure to place it where it would normally go (this could be improved)
        dropdownMenu.css({
            'display': 'block',
                'top': eOffset.top + $(e.target).outerHeight(),
                'left': eOffset.left
        });
    });

    // and when you hide it, reattach the drop down, and hide it normally                                                   
    $(window).on('hide.bs.dropdown', function (e) {
        $(e.target).append(dropdownMenu.detach());
        dropdownMenu.hide();
    });
})();

EDIT J'ai finalement trouvé où j'avais initialement trouvé cette solution. Je dois donner un crédit où le crédit est dû!

17
Jonathan

Pour ceux qui, comme moi, ont le même problème avec Angular 6+ et Bootstrap 4+, j'ai écrit une petite directive visant à ajouter la liste déroulante au corps:

events.ts

/**
 * Add a jQuery listener for a specified HTML event.
 * When an event is received, emit it again in the standard way, and not using jQuery (like Bootstrap does).
 *
 * @param event Event to relay
 * @param node HTML node (default is body)
 *
 * https://stackoverflow.com/a/24212373/2611798
 * https://stackoverflow.com/a/46458318/2611798
 */
export function eventRelay(event: any, node: HTMLElement = document.body) {
    $(node).on(event, (evt: any) => {
        const customEvent = document.createEvent("Event");
        customEvent.initEvent(event, true, true);
        evt.target.dispatchEvent(customEvent);
    });
}

dropdown-body.directive.ts

import {Directive, ElementRef, AfterViewInit, Renderer2} from "@angular/core";
import {fromEvent} from "rxjs";

import {eventRelay} from "../shared/dom/events";

/**
 * Directive used to display a dropdown by attaching it as a body child and not a child of the current node.
 *
 * Sources :
 * <ul>
 *  <li>https://getbootstrap.com/docs/4.1/components/dropdowns/</li>
 *  <li>https://stackoverflow.com/a/42498168/2611798</li>
 *  <li>https://github.com/ng-bootstrap/ng-bootstrap/issues/1012</li>
 * </ul>
 */
@Directive({
    selector: "[appDropdownBody]"
})
export class DropdownBodyDirective implements AfterViewInit {

    /**
     * Dropdown
     */
    private dropdown: HTMLElement;

    /**
     * Dropdown menu
     */
    private dropdownMenu: HTMLElement;

    constructor(private readonly element: ElementRef, private readonly renderer: Renderer2) {
    }

    ngAfterViewInit() {
        this.dropdown = this.element.nativeElement;
        this.dropdownMenu = this.dropdown.querySelector(".dropdown-menu");

        // Catch the events using observables
        eventRelay("shown.bs.dropdown", this.element.nativeElement);
        eventRelay("hidden.bs.dropdown", this.element.nativeElement);

        fromEvent(this.element.nativeElement, "shown.bs.dropdown")
            .subscribe(() => this.appendDropdownMenu(document.body));
        fromEvent(this.element.nativeElement, "hidden.bs.dropdown")
            .subscribe(() => this.appendDropdownMenu(this.dropdown));
    }

    /**
     * Append the dropdown to the "parent" node.
     *
     * @param parent New dropdown parent node
     */
    protected appendDropdownMenu(parent: HTMLElement): void {
        this.renderer.appendChild(parent, this.dropdownMenu);
    }
}

dropdown-body.directive.spec.ts

import {Component, DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";
import {from} from "rxjs";

import {TestBed, ComponentFixture, async} from "@angular/core/testing";

import {DropdownBodyDirective} from "./dropdown-body.directive";

@Component({
    template: `<div class="btn-group dropdown" appDropdownBody>
        <button id="openBtn" data-toggle="dropdown">open</button>
        <div class="dropdown-menu">
            <button class="dropdown-item">btn0</button>
            <button class="dropdown-item">btn1</button>
        </div>
    </div>`
})
class DropdownContainerTestingComponent {
}

describe("DropdownBodyDirective", () => {

    let component: DropdownContainerTestingComponent;
    let fixture: ComponentFixture<DropdownContainerTestingComponent>;
    let dropdown: DebugElement;
    let dropdownMenu: DebugElement;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [
                DropdownContainerTestingComponent,
                DropdownBodyDirective,
            ]
        });
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(DropdownContainerTestingComponent);
        component = fixture.componentInstance;
        dropdown = fixture.debugElement.query(By.css(".dropdown"));
        dropdownMenu = fixture.debugElement.query(By.css(".dropdown-menu"));
    });

    it("should create an instance", () => {
        fixture.detectChanges();
        expect(component).toBeTruthy();

        expect(dropdownMenu.parent).toEqual(dropdown);
    });

    it("not shown", () => {
        fixture.detectChanges();

        expect(dropdownMenu.parent).toEqual(dropdown);
    });

    it("show then hide", () => {
        fixture.detectChanges();
        const nbChildrenBeforeShow = document.body.children.length;

        expect(dropdownMenu.parent).toEqual(dropdown);

        // Simulate the dropdown display event
        dropdown.nativeElement.dispatchEvent(new Event("shown.bs.dropdown"));
        fixture.detectChanges();

        from(fixture.whenStable()).subscribe(() => {
            // Check the dropdown is attached to the body
            expect(document.body.children.length).toEqual(nbChildrenBeforeShow + 1);
            expect(dropdownMenu.nativeElement.parentNode.outerHTML)
                .toBe(document.body.outerHTML);

            // Hide the dropdown
            dropdown.nativeElement.dispatchEvent(new Event("hidden.bs.dropdown"));
            fixture.detectChanges();

            from(fixture.whenStable()).subscribe(() => {
                // Check the dropdown is back to its original node
                expect(document.body.children.length).toEqual(nbChildrenBeforeShow);
                expect(dropdownMenu.nativeElement.parentNode.outerHTML)
                    .toBe(dropdown.nativeElement.outerHTML);
            });
        });
    });
});
0
Junior Dussouillez