如何在 Bootstrap 中将单个下拉菜单附加到正文

Posted

技术标签:

【中文标题】如何在 Bootstrap 中将单个下拉菜单附加到正文【英文标题】:How to append a single dropdown menu to body in Bootstrap 【发布时间】:2015-09-10 19:37:18 【问题描述】:

我已经看过下拉菜单 as component 和 separately using javascript 的文档。

我想知道是否可以在网站正文中添加一个下拉菜单(绝对相对于可点击按钮元素定位)。

为什么?

因为如果我有一个包含 500 行的表,我不想将相同的 10 项列表添加 500 次,从而在处理 JS 时导致生成的 html 更大更慢。

因为可以隐藏父元素,但我仍然希望下拉菜单可见,直到他们在外部单击使其失去焦点。

我找到了更多人 asking for this feature,但我在文档中找不到任何关于它的内容。

【问题讨论】:

你有没有尝试过?入门小提琴会很有用。 我会尝试的任何事情都是“黑客”。我在用引导程序本身“我想知道这是否可能”。 【参考方案1】:

正如bootstrap documents 所说,没有下拉菜单选项...这很可悲,但这意味着目前没有针对您想要的功能的“引导”解决方案。 不过,如果您正在使用 Angular-UI/Bootstrap 套件,现在有一个解决方案。您引用的票证已关闭,因为截至 2015 年 7 月 15 日,它终于是 added to the Angular-UI。

您所要做的就是将下拉附加到正文添加到下拉元素以附加到正文的内部下拉菜单。当下拉按钮位于具有溢出:隐藏的 div 内时,这很有用,否则菜单将被隐藏。 (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>

希望这会有所帮助!


编辑

为了回答另一个 SO 问题,我找到了一个在您不使用 Angular-UI 时效果很好的解决方案。它可能有点“hacky”,但它不会破坏引导菜单功能,而且它似乎在我用过的大多数用例中都能很好地发挥作用。

所以我会留下一些小提琴,以防其他人看到并感兴趣。第一个说明了为什么使用正文附加菜单可能很好,第二个说明了可行的解决方案:

Problem FIDDLE

问题:面板主体内的选择下拉菜单

<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();
    );
)();

编辑 我终于找到了我最初找到这个解决方案的地方。必须感谢credit is due!

【讨论】:

感谢您的回复。 I asked it on bootstrap issues forum 并获得了相当不错的重播。我关闭了话题,但他们又打开了,看来他们可能会考虑看看! 我明白你为什么需要它了! Angular-UI 解决方案可能不适用于那时......但“hacky”解决方案会。它不应该破坏任何东西。希望 Bootstrap 能够使用该程序并为我们提供我们想要的东西!这将是一个不错的功能! 另外,您对上下文菜单事件的引导菜单的使用非常巧妙!我必须记住以备将来使用! 此解决方案的唯一小问题是,如果在下拉菜单打开(或调整大小等)时可能导致原始容器移动,则菜单不会随之移动。不过,不应该经常发生…… 这是我想使用的解决方案,但是在调用事件之后,lib 本身的 ddropdown 放置(设置 css)会覆盖我可能设置的任何 css,即使使用显示的事件也是如此节目的【参考方案2】:

对于像我这样在使用 Angular 6+ 和 Bootstrap 4+ 时遇到同样问题的人,我编写了一个小指令将下拉菜单附加到正文:

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://***.com/a/24212373/2611798
 * https://***.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://***.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);
            );
        );
    );
);

【讨论】:

【参考方案3】:

不确定 bootstrap 3,但如果您使用的是 bootstrap 4,您可以将“data-boundary="window" 添加到下拉触发器中。它会将其附加到正文中,然后您可以使用绝对定位对其进行定位.

【讨论】:

我真的不这么认为,因为它只是向 html 元素添加一个属性,除了引导程序中已有的之外,不需要额外的 js。【参考方案4】:

使用 bootstrap 4,您可以像这样将下拉菜单放在外面:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>

<!-- Assuming a big table -->
<table>
  <tr>
    <td>
      <!-- data-target is a selector -->
      <a href="#" role="button" data-toggle="dropdown" data-target="#dropdown-container" aria-haspopup="true" aria-expanded="false">
            O
          </a>
    </td>
  </tr>
</table>
<div id="dropdown-container">
  <div id="dropdown-menu" class="dropdown-menu">
    <a class="dropdown-item" href="#">Action</a>
    <a class="dropdown-item" href="#">Another action</a>
    <a class="dropdown-item" href="#">Something else here</a>
  </div>
</div>

我必须有一个容器作为目标,才能使 Popper 指向正确的容器。 如果您对放置有疑问,请发表评论,我将添加一个更复杂的解决方案,我必须实施覆盖 Popper 放置。

【讨论】:

以上是关于如何在 Bootstrap 中将单个下拉菜单附加到正文的主要内容,如果未能解决你的问题,请参考以下文章

Bootstrap 4 灵活的响应式导航栏菜单

如何在 XCode 5 中将类附加到情节提要

如何创建 Javascript 函数来管理(Bootstrap)下拉菜单点击,而不是创建大量侦听器?

Bootstrap 多选下拉菜单,在 Rails 上带有搜索栏

将图像添加到引导下拉菜单

如何在导航栏中制作 Bootstrap 4 全宽下拉菜单?