Skip to content

Commit 7ed199d

Browse files
authored
feat: dom utility methods (bootstrap-vue#1013)
* feat: dom utility methods * Update index.js * [tooltip.js] Use dom utils * [dropdown.js] Use dom utils * [scrollspy.js] Use dom utils
1 parent 31a71fd commit 7ed199d

File tree

5 files changed

+96
-132
lines changed

5 files changed

+96
-132
lines changed

lib/classes/tooltip.js

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Popper from 'popper.js';
22
import { assign, keys } from '../utils/object';
33
import { from as arrayFrom } from '../utils/array';
4+
import { closest, isVisible, isDisabled } from '../utils/dom';
45
import BvEvent from './BvEvent';
56

67
const inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -80,38 +81,6 @@ function generateId(name) {
8081
return `__BV_${name}_${NEXTID++}__`;
8182
}
8283

83-
// Determine if an element is visible. Faster than CSS checks
84-
function elVisible(el) {
85-
return el &&
86-
document.body.contains(el) &&
87-
el.offsetParent !== null &&
88-
(el.offsetWidth > 0 || el.offsetHeight > 0);
89-
}
90-
91-
// Determine if an element is disabled
92-
function elDisabled(el) {
93-
return !el || el.disabled || el.classList.contains('disabled') || Boolean(el.getAttribute('disabled'));
94-
}
95-
96-
/*
97-
* Polyfill for Element.closest() for IE :(
98-
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
99-
*/
100-
if (inBrowser && window.Element && !Element.prototype.closest) {
101-
Element.prototype.closest = function (s) {
102-
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
103-
let el = this;
104-
let i;
105-
do {
106-
i = matches.length;
107-
// eslint-disable-next-line no-empty
108-
while (--i >= 0 && matches.item(i) !== el) {
109-
}
110-
} while ((i < 0) && (el = el.parentElement));
111-
return el;
112-
};
113-
}
114-
11584
/*
11685
* ToolTip Class definition
11786
*/
@@ -311,7 +280,7 @@ class ToolTip {
311280
if (on) {
312281
this.$visibleInterval = setInterval(() => {
313282
const tip = this.getTipElement();
314-
if (tip && !elVisible(this.$element) && tip.classList.contains(ClassName.SHOW)) {
283+
if (tip && !isVisible(this.$element) && tip.classList.contains(ClassName.SHOW)) {
315284
// Element is no longer visible, so force-hide the tooltip
316285
this.forceHide();
317286
}
@@ -610,7 +579,7 @@ class ToolTip {
610579

611580
handleEvent(e) {
612581
// This special method allows us to use "this" as the event handlers
613-
if (elDisabled(this.$element)) {
582+
if (isDisabled(this.$element)) {
614583
// If disabled, don't do anything. Note: if tip is shown before element gets
615584
// disabled, then tip not close until no longer disabled or forcefully closed.
616585
return;
@@ -646,7 +615,7 @@ class ToolTip {
646615
}
647616

648617
setModalListener(on) {
649-
const modal = this.$element.closest(MODAL_CLASS);
618+
const modal = closest(MODAL_CLASS, this.$element);
650619
if (!modal) {
651620
// If we are not in a modal, don't worry. be happy
652621
return;

lib/directives/scrollspy.js

Lines changed: 10 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,10 @@
11
import { isArray, from as arrayFrom } from '../utils/array';
22
import { assign, keys } from '../utils/object';
3+
import { isElement, closest, selectAll, select } from '../utils/dom';
4+
35
const inBrowser = typeof window !== 'undefined';
46
const isServer = !inBrowser;
57

6-
/*
7-
* Polyfill for Element.closest() for IE :(
8-
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
9-
*/
10-
11-
if (inBrowser && window.Element && !Element.prototype.closest) {
12-
Element.prototype.closest = function (s) {
13-
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
14-
let el = this;
15-
let i;
16-
do {
17-
i = matches.length;
18-
// eslint-disable-next-line no-empty
19-
while (--i >= 0 && matches.item(i) !== el) {
20-
}
21-
} while ((i < 0) && (el = el.parentElement));
22-
return el;
23-
};
24-
}
25-
268
/*
279
* Constants / Defaults
2810
*/
@@ -71,42 +53,6 @@ const OffsetMethod = {
7153
POSITION: 'position'
7254
};
7355

74-
/*
75-
* DOM Utility Methods
76-
*/
77-
78-
function isElement(obj) {
79-
return obj.nodeType;
80-
}
81-
82-
// Wrapper for Element.closest to emulate jQuery's closest (sorta)
83-
function closest(element, selector) {
84-
const el = element.closest(selector);
85-
return el === element ? null : el;
86-
}
87-
88-
// Query Selector All wrapper
89-
function $QSA(selector, element) {
90-
if (!element) {
91-
element = document;
92-
}
93-
if (!isElement(element)) {
94-
return [];
95-
}
96-
return arrayFrom(element.querySelectorAll(selector));
97-
}
98-
99-
// Query Selector wrapper
100-
function $QS(selector, element) {
101-
if (!element) {
102-
element = document;
103-
}
104-
if (!isElement(element)) {
105-
return null;
106-
}
107-
return element.querySelector(selector) || null;
108-
}
109-
11056
/*
11157
* Utility Methods
11258
*/
@@ -269,10 +215,10 @@ ScrollSpy.prototype.refresh = function () {
269215
this._scrollHeight = this._getScrollHeight();
270216

271217
// Find all nav link/dropdown/list-item links in our element
272-
$QSA(this._selector, this._$el).map(el => {
218+
selectAll(this._selector, this._$el).map(el => {
273219
const href = el.getAttribute('href');
274220
if (href && href.charAt(0) === '#' && href !== '#' && href.indexOf('#/') === -1) {
275-
const target = $QS(href, scroller);
221+
const target = select(href, scroller);
276222
if (!target) {
277223
return null;
278224
}
@@ -406,7 +352,7 @@ ScrollSpy.prototype._getScroller = function () {
406352
return document.body;
407353
}
408354
// Otherwise assume CSS selector
409-
return $QS(scroller);
355+
return select(scroller);
410356
}
411357
return null;
412358
};
@@ -450,14 +396,14 @@ ScrollSpy.prototype._activate = function (target) {
450396
return selector + '[href="' + target + '"]';
451397
});
452398

453-
const links = $QSA(queries.join(','), this._$el);
399+
const links = selectAll(queries.join(','), this._$el);
454400

455401
links.forEach(link => {
456402
if (link.classList.contains(ClassName.DROPDOWN_ITEM)) {
457403
// This is a dropdown item, so find the .dropdown-toggle and set it's state
458-
const dropdown = closest(link, Selector.DROPDOWN);
404+
const dropdown = closest(Selector.DROPDOWN, link);
459405
if (dropdown) {
460-
const toggle = $QS(Selector.DROPDOWN_TOGGLE, dropdown);
406+
const toggle = select(Selector.DROPDOWN_TOGGLE, dropdown);
461407
if (toggle) {
462408
this._setActiveState(toggle, true);
463409
}
@@ -482,7 +428,7 @@ ScrollSpy.prototype._activate = function (target) {
482428

483429
// Clear the 'active' targets in our nav component
484430
ScrollSpy.prototype._clear = function () {
485-
$QSA(this._selector, this._$el).filter(el => {
431+
selectAll(this._selector, this._$el).filter(el => {
486432
if (el.classList.contains(ClassName.ACTIVE)) {
487433
const href = el.getAttribute('href');
488434
if (href.charAt(0) !== '#' || href.indexOf('#/') === 0) {
@@ -522,7 +468,7 @@ ScrollSpy.prototype._setParentsSiblingActiveState = function (element, selector,
522468
}
523469
let el = element;
524470
while (el) {
525-
el = closest(el, selector);
471+
el = closest(selector, el);
526472
if (el && el.previousElementSibling) {
527473
for (let i = 0; i < classes.length - 1; i++) {
528474
if (el.previousElementSibling.classList.contains(classes[i])) {

lib/mixins/dropdown.js

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,13 @@ import clickoutMixin from "./clickout";
33
import listenOnRootMixin from "./listen-on-root";
44
import { from as arrayFrom } from "../utils/array";
55
import { assign } from "../utils/object";
6-
7-
// Determine if an HTML element is visible - Faster than CSS check
8-
function isVisible(el) {
9-
return el && (el.offsetWidth > 0 || el.offsetHeight > 0);
10-
}
6+
import { isVisible, closest, selectAll } from "../utils/dom";
117

128
// Return an Array of visible items
139
function filterVisible(els) {
1410
return (els || []).filter(isVisible);
1511
}
1612

17-
// Element closest polyfill, if needed
18-
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
19-
// Returns null of not found
20-
if (typeof document !== "undefined" && window.Element && !Element.prototype.closest) {
21-
Element.prototype.closest = function(s) {
22-
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
23-
let el = this;
24-
let i;
25-
do {
26-
i = matches.length;
27-
// eslint-disable-next-line no-empty
28-
while (--i >= 0 && matches.item(i) !== el) {}
29-
} while (i < 0 && (el = el.parentElement));
30-
return el;
31-
};
32-
}
33-
3413
// Dropdown item CSS selectors
3514
// TODO: .dropdown-form handling
3615
const ITEM_SELECTOR = ".dropdown-item:not(.disabled):not([disabled])";
@@ -152,7 +131,7 @@ export default {
152131
if (typeof Popper === "function") {
153132
// Are we in a navbar ?
154133
if (this.inNavbar === null && this.isNav) {
155-
this.inNavbar = Boolean(this.$el.closest(".navbar"));
134+
this.inNavbar = Boolean(closest(".navbar", this.$el));
156135
}
157136
// for dropup with alignment we use the parent element as popper container
158137
let element = ((this.dropup && this.right) || this.split || this.inNavbar) ? this.$el : this.$refs.toggle;
@@ -211,7 +190,6 @@ export default {
211190
return assign(popperConfig, this.popperOpts || {});
212191
},
213192
setTouchStart(on) {
214-
215193
/*
216194
If this is a touch-enabled device we add extra
217195
empty mouseover listeners to the body's immediate children;
@@ -221,15 +199,11 @@ export default {
221199
if ("ontouchstart" in document.documentElement) {
222200
const children = arrayFrom(document.body.children);
223201
children.forEach(el => {
224-
if (on) {
225-
el.addEventListener("mouseover", this.noop);
226-
} else {
227-
el.removeEventListener("mouseover", this.noop);
228-
}
202+
el[on ? "addEventListener" : "removeEventListener"]("mouseover", this._noop);
229203
});
230204
}
231205
},
232-
noop() {
206+
_noop() {
233207
// Do nothing event handler (used in touchstart event handler)
234208
},
235209
clickOutListener() {
@@ -291,8 +265,7 @@ export default {
291265
},
292266
onMouseOver(evt) {
293267
// Focus the item on hover
294-
// TODO: Special handling for inputs?
295-
// Inputs are in a special .dropdown-form container
268+
// TODO: Special handling for inputs? Inputs are in a special .dropdown-form container
296269
const item = evt.target;
297270
if (
298271
item.classList.contains("dropdown-item") &&
@@ -334,7 +307,7 @@ export default {
334307
},
335308
getItems() {
336309
// Get all items
337-
return filterVisible(arrayFrom(this.$refs.menu.querySelectorAll(ITEM_SELECTOR)));
310+
return filterVisible(selectAll(ITEM_SELECTOR, this.$refs.menu));
338311
},
339312
getFirstItem() {
340313
// Get the first non-disabled item

lib/utils/dom.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { from as arrayFrom } from './array';
2+
3+
/*
4+
* Element closest polyfill, if needed
5+
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
6+
* Returns null of not found
7+
*/
8+
if (typeof document !== "undefined" && window.Element && !Element.prototype.closest) {
9+
Element.prototype.closest = function(s) {
10+
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
11+
let el = this;
12+
let i;
13+
do {
14+
i = matches.length;
15+
// eslint-disable-next-line no-empty
16+
while (--i >= 0 && matches.item(i) !== el) {}
17+
} while (i < 0 && (el = el.parentElement));
18+
return el;
19+
};
20+
}
21+
22+
const dom = {};
23+
24+
// Determine if an element is an HTML Element
25+
dom.isElement = function(el) {
26+
return el && el.nodeType === Node.ELEMENT_NODE;
27+
};
28+
29+
// Determine if an HTML element is visible - Faster than CSS check
30+
dom.isVisible = function(el) {
31+
return dom.isElement(el) &&
32+
document.body.contains(el) &&
33+
(el.offsetParent !== null || el.offsetWidth > 0 || el.offsetHeight > 0);
34+
};
35+
36+
// Determine if an element is disabled
37+
dom.isDisabled = function(el) {
38+
return !dom.isElemetn(el) ||
39+
el.disabled ||
40+
el.classList.contains('disabled') ||
41+
Boolean(el.getAttribute('disabled'));
42+
};
43+
44+
// Select all elements matching selector. Returns [] if none found
45+
dom.selectAll = function(selector, root) {
46+
if (!dom.isElement(root)) {
47+
root = document;
48+
}
49+
return arrayFrom(root.querySelectorAll(selector));
50+
};
51+
52+
// Select a single element, returns null if not found
53+
dom.select = function(selector, root) {
54+
if (!dom.isElement(root)) {
55+
root = document;
56+
}
57+
return root.querySelector(selector) || null;
58+
};
59+
60+
// Finds closest element matching selector. Returns null if not found
61+
dom.closest = function(selector, root) {
62+
if (!dom.isElement(root)) {
63+
return null;
64+
}
65+
const el = root.closest(selector);
66+
return el === root ? null : el;
67+
};
68+
69+
export const isElement = dom.isElement;
70+
export const isVisible = dom.isVisible;
71+
export const isDisabled = dom.isDisabled;
72+
export const closest = dom.closest;
73+
export const selectAll = dom.selectAll;
74+
export const select = dom.select;

lib/utils/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import addEventListenerOnce from "./addEventListenerOnce";
22
import * as array from "./array";
33
import * as object from "./object";
4+
import * as dom from "./dom";
45
import copyProps from "./copyProps";
56
import lowerFirst from "./lowerFirst";
67
import identity from "./identity";
@@ -18,6 +19,7 @@ export {
1819
addEventListenerOnce,
1920
array,
2021
copyProps,
22+
dom,
2123
lowerFirst,
2224
identity,
2325
mergeData,

0 commit comments

Comments
 (0)