UNPKG

bootstrap-vue

Version:

Quickly integrate Bootstrap 4 components with Vue.js

339 lines (323 loc) 11.3 kB
import Popper from "popper.js"; import clickoutMixin from "./clickout"; import listenOnRootMixin from "./listen-on-root"; import { from as arrayFrom } from "../utils/array"; import { assign } from "../utils/object"; import { isVisible, closest, selectAll, getAttr, setAttr, eventOn, eventOff } from "../utils/dom"; // Return an Array of visible items function filterVisible(els) { return (els || []).filter(isVisible); } // Dropdown item CSS selectors // TODO: .dropdown-form handling var ITEM_SELECTOR = ".dropdown-item:not(.disabled):not([disabled])"; // Popper attachment positions var AttachmentMap = { // DropUp Left Align TOP: "top-start", // DropUp Right Align TOPEND: "top-end", // Dropdown left Align BOTTOM: "bottom-start", // Dropdown Right Align BOTTOMEND: "bottom-end" }; export default { mixins: [clickoutMixin, listenOnRootMixin], props: { disabled: { type: Boolean, default: false }, text: { // Button label type: String, default: "" }, dropup: { // place on top if possible type: Boolean, default: false }, right: { // Right align menu (default is left align) type: Boolean, default: false }, offset: { // Number of pixels to offset menu, or a CSS unit value (i.e. 1px, 1rem, etc) type: [Number, String], default: 0 }, noFlip: { // Disable auto-flipping of menu from bottom<=>top type: Boolean, default: false }, popperOpts: { type: Object, default: function _default() {} } }, data: function data() { return { visible: false, _popper: null, inNavbar: null }; }, created: function created() { var _this = this; var listener = function listener(el) { if (el !== _this) { _this.visible = false; } }; // To keep one dropdown opened on page this.listenOnRoot("bv::dropdown::shown", listener); // Hide when clicked on links this.listenOnRoot("clicked::link", listener); // Use new namespaced events this.listenOnRoot("bv::link::clicked", listener); }, watch: { visible: function visible(state, old) { if (state === old) { // Avoid duplicated emits return; } if (state) { this.showMenu(); } else { this.hideMenu(); } }, disabled: function disabled(state, old) { if (state !== old && state && this.visible) { // Hide dropdown if disabled changes to true this.visible = false; } } }, computed: { toggler: function toggler() { return this.$refs.toggle.$el || this.$refs.toggle; } }, destroyed: function destroyed() { if (this._popper) { // Ensure popper event listeners are removed cleanly this._popper.destroy(); } this._popper = null; this.setTouchStart(false); }, methods: { showMenu: function showMenu() { if (this.disabled) { return; } // TODO: move emit show to visible watcher, to allow cancelling of show this.$emit("show"); // Ensure other menus are closed this.emitOnRoot("bv::dropdown::shown", this); // If popper not installed, then fallback gracefully to dropdown only with left alignment if (typeof Popper === "function") { // Are we in a navbar ? if (this.inNavbar === null && this.isNav) { this.inNavbar = Boolean(closest(".navbar", this.$el)); } // for dropup with alignment we use the parent element as popper container var element = this.dropup && this.right || this.split || this.inNavbar ? this.$el : this.$refs.toggle; // Make sure we have a reference to an element, not a component! element = element.$el || element; // Instantiate popper.js this._popper = new Popper(element, this.$refs.menu, this.getPopperConfig()); } this.setTouchStart(true); this.$emit("shown"); // Focus on the first item on show this.$nextTick(this.focusFirstItem); }, hideMenu: function hideMenu() { // TODO: move emit hide to visible watcher, to allow cancelling of hide this.$emit("hide"); if (this._popper) { // Ensure popper event listeners are removed cleanly this._popper.destroy(); } this._popper = null; this.setTouchStart(false); this.emitOnRoot("bv::dropdown::hidden", this); this.$emit("hidden"); }, getPopperConfig: function getPopperConfig() { var placement = AttachmentMap.BOTTOM; if (this.dropup && this.right) { // dropup + right placement = AttachmentMap.TOPEND; } else if (this.dropup) { // dropup + left placement = AttachmentMap.TOP; } else if (this.right) { // dropdown + right placement = AttachmentMap.BOTTOMEND; } var popperConfig = { placement: placement, modifiers: { offset: { offset: this.offset || 0 }, flip: { enabled: !this.noFlip }, applyStyle: { // Disable Popper.js for Dropdown in Navbar enabled: !this.inNavbar } } }; return assign(popperConfig, this.popperOpts || {}); }, setTouchStart: function setTouchStart(on) { var _this2 = this; /* If this is a touch-enabled device we add extra empty mouseover listeners to the body's immediate children; only needed because of broken event delegation on iOS https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html */ if ("ontouchstart" in document.documentElement) { var children = arrayFrom(document.body.children); children.forEach(function (el) { if (on) { eventOn("mouseover", _this2._noop); } else { eventOff("mouseover", _this2._noop); } }); } }, _noop: function _noop() { // Do nothing event handler (used in touchstart event handler) }, clickOutListener: function clickOutListener() { this.visible = false; }, click: function click(e) { // Calle only in split button mode, for the split button if (this.disabled) { this.visible = false; return; } this.$emit("click", e); }, toggle: function toggle() { // Called only by a button that toggles the menu if (this.disabled) { this.visible = false; return; } this.visible = !this.visible; }, show: function show() { // Public method to show dropdown if (this.disabled) { return; } this.visible = true; }, hide: function hide() { // Public method to hide dropdown if (this.disabled) { return; } this.visible = false; }, onTab: function onTab() { if (this.visible) { // TODO: Need special handler for dealing with form inputs // Tab, if in a text-like input, we should just focus next item in the dropdown // Note: Inputs are in a special .dropdown-form container this.visible = false; } }, onEsc: function onEsc(e) { if (this.visible) { this.visible = false; e.preventDefault(); e.stopPropagation(); // Return focus to original trigger button this.$nextTick(this.focusToggler); } }, onFocusOut: function onFocusOut(evt) { if (this.$refs.menu.contains(evt.relatedTarget)) { return; } this.visible = false; }, onMouseOver: function onMouseOver(evt) { // Focus the item on hover // TODO: Special handling for inputs? Inputs are in a special .dropdown-form container var item = evt.target; if (item.classList.contains("dropdown-item") && !item.disabled && !item.classList.contains("disabled") && item.focus) { item.focus(); } }, focusNext: function focusNext(e, up) { var _this3 = this; if (!this.visible) { return; } e.preventDefault(); e.stopPropagation(); this.$nextTick(function () { var items = _this3.getItems(); if (items.length < 1) { return; } var index = items.indexOf(e.target); if (up && index > 0) { index--; } else if (!up && index < items.length - 1) { index++; } if (index < 0) { index = 0; } _this3.focusItem(index, items); }); }, focusItem: function focusItem(idx, items) { var el = items.find(function (el, i) { return i === idx; }); if (el && getAttr(el, "tabindex") !== "-1") { el.focus(); } }, getItems: function getItems() { // Get all items return filterVisible(selectAll(ITEM_SELECTOR, this.$refs.menu)); }, getFirstItem: function getFirstItem() { // Get the first non-disabled item var item = this.getItems()[0]; return item || null; }, focusFirstItem: function focusFirstItem() { var item = this.getFirstItem(); if (item) { this.focusItem(0, [item]); } }, focusToggler: function focusToggler() { var toggler = this.toggler; if (toggler && toggler.focus) { toggler.focus(); } } } };