Permalink
Browse files

feat: dom utility methods (#1013)

* feat: dom utility methods

* Update index.js

* [tooltip.js] Use dom utils

* [dropdown.js] Use dom utils

* [scrollspy.js] Use dom utils
  • Loading branch information...
tmorehouse committed Sep 8, 2017
1 parent 31a71fd commit 7ed199d703e8664ff192345ba9d03ebcb7c3176e
Showing with 96 additions and 132 deletions.
  1. +4 −35 lib/classes/tooltip.js
  2. +10 −64 lib/directives/scrollspy.js
  3. +6 −33 lib/mixins/dropdown.js
  4. +74 −0 lib/utils/dom.js
  5. +2 −0 lib/utils/index.js
@@ -1,6 +1,7 @@
import Popper from 'popper.js';
import { assign, keys } from '../utils/object';
import { from as arrayFrom } from '../utils/array';
import { closest, isVisible, isDisabled } from '../utils/dom';
import BvEvent from './BvEvent';
const inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -80,38 +81,6 @@ function generateId(name) {
return `__BV_${name}_${NEXTID++}__`;
}
// Determine if an element is visible. Faster than CSS checks
function elVisible(el) {
return el &&
document.body.contains(el) &&
el.offsetParent !== null &&
(el.offsetWidth > 0 || el.offsetHeight > 0);
}
// Determine if an element is disabled
function elDisabled(el) {
return !el || el.disabled || el.classList.contains('disabled') || Boolean(el.getAttribute('disabled'));
}
/*
* Polyfill for Element.closest() for IE :(
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
*/
if (inBrowser && window.Element && !Element.prototype.closest) {
Element.prototype.closest = function (s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let el = this;
let i;
do {
i = matches.length;
// eslint-disable-next-line no-empty
while (--i >= 0 && matches.item(i) !== el) {
}
} while ((i < 0) && (el = el.parentElement));
return el;
};
}
/*
* ToolTip Class definition
*/
@@ -311,7 +280,7 @@ class ToolTip {
if (on) {
this.$visibleInterval = setInterval(() => {
const tip = this.getTipElement();
if (tip && !elVisible(this.$element) && tip.classList.contains(ClassName.SHOW)) {
if (tip && !isVisible(this.$element) && tip.classList.contains(ClassName.SHOW)) {
// Element is no longer visible, so force-hide the tooltip
this.forceHide();
}
@@ -610,7 +579,7 @@ class ToolTip {
handleEvent(e) {
// This special method allows us to use "this" as the event handlers
if (elDisabled(this.$element)) {
if (isDisabled(this.$element)) {
// If disabled, don't do anything. Note: if tip is shown before element gets
// disabled, then tip not close until no longer disabled or forcefully closed.
return;
@@ -646,7 +615,7 @@ class ToolTip {
}
setModalListener(on) {
const modal = this.$element.closest(MODAL_CLASS);
const modal = closest(MODAL_CLASS, this.$element);
if (!modal) {
// If we are not in a modal, don't worry. be happy
return;
@@ -1,28 +1,10 @@
import { isArray, from as arrayFrom } from '../utils/array';
import { assign, keys } from '../utils/object';
import { isElement, closest, selectAll, select } from '../utils/dom';
const inBrowser = typeof window !== 'undefined';
const isServer = !inBrowser;
/*
* Polyfill for Element.closest() for IE :(
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
*/
if (inBrowser && window.Element && !Element.prototype.closest) {
Element.prototype.closest = function (s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let el = this;
let i;
do {
i = matches.length;
// eslint-disable-next-line no-empty
while (--i >= 0 && matches.item(i) !== el) {
}
} while ((i < 0) && (el = el.parentElement));
return el;
};
}
/*
* Constants / Defaults
*/
@@ -71,42 +53,6 @@ const OffsetMethod = {
POSITION: 'position'
};
/*
* DOM Utility Methods
*/
function isElement(obj) {
return obj.nodeType;
}
// Wrapper for Element.closest to emulate jQuery's closest (sorta)
function closest(element, selector) {
const el = element.closest(selector);
return el === element ? null : el;
}
// Query Selector All wrapper
function $QSA(selector, element) {
if (!element) {
element = document;
}
if (!isElement(element)) {
return [];
}
return arrayFrom(element.querySelectorAll(selector));
}
// Query Selector wrapper
function $QS(selector, element) {
if (!element) {
element = document;
}
if (!isElement(element)) {
return null;
}
return element.querySelector(selector) || null;
}
/*
* Utility Methods
*/
@@ -269,10 +215,10 @@ ScrollSpy.prototype.refresh = function () {
this._scrollHeight = this._getScrollHeight();
// Find all nav link/dropdown/list-item links in our element
$QSA(this._selector, this._$el).map(el => {
selectAll(this._selector, this._$el).map(el => {
const href = el.getAttribute('href');
if (href && href.charAt(0) === '#' && href !== '#' && href.indexOf('#/') === -1) {
const target = $QS(href, scroller);
const target = select(href, scroller);
if (!target) {
return null;
}
@@ -406,7 +352,7 @@ ScrollSpy.prototype._getScroller = function () {
return document.body;
}
// Otherwise assume CSS selector
return $QS(scroller);
return select(scroller);
}
return null;
};
@@ -450,14 +396,14 @@ ScrollSpy.prototype._activate = function (target) {
return selector + '[href="' + target + '"]';
});
const links = $QSA(queries.join(','), this._$el);
const links = selectAll(queries.join(','), this._$el);
links.forEach(link => {
if (link.classList.contains(ClassName.DROPDOWN_ITEM)) {
// This is a dropdown item, so find the .dropdown-toggle and set it's state
const dropdown = closest(link, Selector.DROPDOWN);
const dropdown = closest(Selector.DROPDOWN, link);
if (dropdown) {
const toggle = $QS(Selector.DROPDOWN_TOGGLE, dropdown);
const toggle = select(Selector.DROPDOWN_TOGGLE, dropdown);
if (toggle) {
this._setActiveState(toggle, true);
}
@@ -482,7 +428,7 @@ ScrollSpy.prototype._activate = function (target) {
// Clear the 'active' targets in our nav component
ScrollSpy.prototype._clear = function () {
$QSA(this._selector, this._$el).filter(el => {
selectAll(this._selector, this._$el).filter(el => {
if (el.classList.contains(ClassName.ACTIVE)) {
const href = el.getAttribute('href');
if (href.charAt(0) !== '#' || href.indexOf('#/') === 0) {
@@ -522,7 +468,7 @@ ScrollSpy.prototype._setParentsSiblingActiveState = function (element, selector,
}
let el = element;
while (el) {
el = closest(el, selector);
el = closest(selector, el);
if (el && el.previousElementSibling) {
for (let i = 0; i < classes.length - 1; i++) {
if (el.previousElementSibling.classList.contains(classes[i])) {
@@ -3,34 +3,13 @@ import clickoutMixin from "./clickout";
import listenOnRootMixin from "./listen-on-root";
import { from as arrayFrom } from "../utils/array";
import { assign } from "../utils/object";
// Determine if an HTML element is visible - Faster than CSS check
function isVisible(el) {
return el && (el.offsetWidth > 0 || el.offsetHeight > 0);
}
import { isVisible, closest, selectAll } from "../utils/dom";
// Return an Array of visible items
function filterVisible(els) {
return (els || []).filter(isVisible);
}
// Element closest polyfill, if needed
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
// Returns null of not found
if (typeof document !== "undefined" && window.Element && !Element.prototype.closest) {
Element.prototype.closest = function(s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let el = this;
let i;
do {
i = matches.length;
// eslint-disable-next-line no-empty
while (--i >= 0 && matches.item(i) !== el) {}
} while (i < 0 && (el = el.parentElement));
return el;
};
}
// Dropdown item CSS selectors
// TODO: .dropdown-form handling
const ITEM_SELECTOR = ".dropdown-item:not(.disabled):not([disabled])";
@@ -152,7 +131,7 @@ export default {
if (typeof Popper === "function") {
// Are we in a navbar ?
if (this.inNavbar === null && this.isNav) {
this.inNavbar = Boolean(this.$el.closest(".navbar"));
this.inNavbar = Boolean(closest(".navbar", this.$el));
}
// for dropup with alignment we use the parent element as popper container
let element = ((this.dropup && this.right) || this.split || this.inNavbar) ? this.$el : this.$refs.toggle;
@@ -211,7 +190,6 @@ export default {
return assign(popperConfig, this.popperOpts || {});
},
setTouchStart(on) {
/*
If this is a touch-enabled device we add extra
empty mouseover listeners to the body's immediate children;
@@ -221,15 +199,11 @@ export default {
if ("ontouchstart" in document.documentElement) {
const children = arrayFrom(document.body.children);
children.forEach(el => {
if (on) {
el.addEventListener("mouseover", this.noop);
} else {
el.removeEventListener("mouseover", this.noop);
}
el[on ? "addEventListener" : "removeEventListener"]("mouseover", this._noop);
});
}
},
noop() {
_noop() {
// Do nothing event handler (used in touchstart event handler)
},
clickOutListener() {
@@ -291,8 +265,7 @@ export default {
},
onMouseOver(evt) {
// Focus the item on hover
// TODO: Special handling for inputs?
// Inputs are in a special .dropdown-form container
// TODO: Special handling for inputs? Inputs are in a special .dropdown-form container
const item = evt.target;
if (
item.classList.contains("dropdown-item") &&
@@ -334,7 +307,7 @@ export default {
},
getItems() {
// Get all items
return filterVisible(arrayFrom(this.$refs.menu.querySelectorAll(ITEM_SELECTOR)));
return filterVisible(selectAll(ITEM_SELECTOR, this.$refs.menu));
},
getFirstItem() {
// Get the first non-disabled item
@@ -0,0 +1,74 @@
import { from as arrayFrom } from './array';
/*
* Element closest polyfill, if needed
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
* Returns null of not found
*/
if (typeof document !== "undefined" && window.Element && !Element.prototype.closest) {
Element.prototype.closest = function(s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
let el = this;
let i;
do {
i = matches.length;
// eslint-disable-next-line no-empty
while (--i >= 0 && matches.item(i) !== el) {}
} while (i < 0 && (el = el.parentElement));
return el;
};
}
const dom = {};
// Determine if an element is an HTML Element
dom.isElement = function(el) {
return el && el.nodeType === Node.ELEMENT_NODE;
};
// Determine if an HTML element is visible - Faster than CSS check
dom.isVisible = function(el) {
return dom.isElement(el) &&
document.body.contains(el) &&
(el.offsetParent !== null || el.offsetWidth > 0 || el.offsetHeight > 0);
};
// Determine if an element is disabled
dom.isDisabled = function(el) {
return !dom.isElemetn(el) ||
el.disabled ||
el.classList.contains('disabled') ||
Boolean(el.getAttribute('disabled'));
};
// Select all elements matching selector. Returns [] if none found
dom.selectAll = function(selector, root) {
if (!dom.isElement(root)) {
root = document;
}
return arrayFrom(root.querySelectorAll(selector));
};
// Select a single element, returns null if not found
dom.select = function(selector, root) {
if (!dom.isElement(root)) {
root = document;
}
return root.querySelector(selector) || null;
};
// Finds closest element matching selector. Returns null if not found
dom.closest = function(selector, root) {
if (!dom.isElement(root)) {
return null;
}
const el = root.closest(selector);
return el === root ? null : el;
};
export const isElement = dom.isElement;
export const isVisible = dom.isVisible;
export const isDisabled = dom.isDisabled;
export const closest = dom.closest;
export const selectAll = dom.selectAll;
export const select = dom.select;
@@ -1,6 +1,7 @@
import addEventListenerOnce from "./addEventListenerOnce";
import * as array from "./array";
import * as object from "./object";
import * as dom from "./dom";
import copyProps from "./copyProps";
import lowerFirst from "./lowerFirst";
import identity from "./identity";
@@ -18,6 +19,7 @@ export {
addEventListenerOnce,
array,
copyProps,
dom,
lowerFirst,
identity,
mergeData,

0 comments on commit 7ed199d

Please sign in to comment.