How to append a single dropdown menu to body in Bootstrap

后端 未结 3 1742
走了就别回头了
走了就别回头了 2021-02-19 22:48

I\'ve seen the documentation for the dropdown menu as component and separately using javascript.

I\'m wondering if it is possible to add a single dropdown menu in the we

相关标签:
3条回答
  • 2021-02-19 23:11

    Not sure about bootstrap 3, but if you're using bootstrap 4, you can add "data-boundary="window" to the dropdown trigger. It will append it to the body and then you can position it using absolute positioning.

    0 讨论(0)
  • 2021-02-19 23:13

    For those like me who have the same issue using Angular 6+ and Bootstrap 4+, I wrote a small directive to append the dropdown to the body :

    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 讨论(0)
  • 2021-02-19 23:17

    As the bootstrap documents say, there are no options for dropdown menus... This is sad, but it means there is currently not a 'bootstrap' solution for the functionality you want. There is now, however, a solution in the Angular-UI/Bootstrap kit if you're using that. The ticket you referenced is closed because it was finally added to the Angular-UI as of July 15th 2015.

    All you have to do is 'Add dropdown-append-to-body to the dropdown element to append to the inner dropdown-menu to the body. This is useful when the dropdown button is inside a div with overflow: hidden, and the menu would otherwise be hidden.' (reference)

    <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>
    

    Hope this helps!


    EDIT

    In an effort to answer another SO question, I found a solution that works pretty well if you weren't using Angular-UI. It may be 'hacky', but it doesn't break the bootstrap menu functionality, and it seems to play well with most use cases I've used it for.

    So I'll leave a few fiddles in case anyone else sees this and is interested. The first illustrates why the use of a body appended menu might be nice, the second shows the working solution:

    Problem FIDDLE

    The problem: a select dropdown within a panel body

    <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 I finally found where I originally found this solution. Gotta give credit where credit is due!

    0 讨论(0)
提交回复
热议问题