• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uiv-lib / uiv / 6280707140

22 Sep 2023 09:50PM UTC coverage: 86.751%. Remained the same
6280707140

push

github

web-flow
chore(deps): update dependency eslint to v8.50.0

859 of 1065 branches covered (0.0%)

Branch coverage included in aggregate %.

1531 of 1690 relevant lines covered (90.59%)

183.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.01
/src/utils/dom.utils.js
1
import { isExist, isString, isFunction } from './object.utils';
2

3
export const EVENTS = {
27✔
4
  MOUSE_ENTER: 'mouseenter',
5
  MOUSE_LEAVE: 'mouseleave',
6
  MOUSE_DOWN: 'mousedown',
7
  MOUSE_UP: 'mouseup',
8
  FOCUS: 'focus',
9
  BLUR: 'blur',
10
  CLICK: 'click',
11
  INPUT: 'input',
12
  KEY_DOWN: 'keydown',
13
  KEY_UP: 'keyup',
14
  KEY_PRESS: 'keypress',
15
  RESIZE: 'resize',
16
  SCROLL: 'scroll',
17
  TOUCH_START: 'touchstart',
18
  TOUCH_END: 'touchend',
19
};
20

21
export const TRIGGERS = {
27✔
22
  CLICK: 'click',
23
  HOVER: 'hover',
24
  FOCUS: 'focus',
25
  HOVER_FOCUS: 'hover-focus',
26
  OUTSIDE_CLICK: 'outside-click',
27
  MANUAL: 'manual',
28
};
29

30
export const PLACEMENTS = {
27✔
31
  TOP: 'top',
32
  RIGHT: 'right',
33
  BOTTOM: 'bottom',
34
  LEFT: 'left',
35
};
36

37
export function getComputedStyle(el) {
38
  return window.getComputedStyle(el);
122✔
39
}
40

41
export function getViewportSize() {
42
  const width = window.innerWidth || 0;
57!
43
  const height = window.innerHeight || 0;
57!
44
  return { width, height };
57✔
45
}
46

47
let scrollbarWidth = null;
27✔
48
let savedScreenSize = null;
27✔
49

50
export function getScrollbarWidth(recalculate = false) {
2✔
51
  const screenSize = getViewportSize();
4✔
52
  // return directly when already calculated & not force recalculate & screen size not changed
53
  if (
4✔
54
    scrollbarWidth !== null &&
11✔
55
    !recalculate &&
56
    screenSize.height === savedScreenSize.height &&
57
    screenSize.width === savedScreenSize.width
58
  ) {
59
    return scrollbarWidth;
2✔
60
  }
61
  /* istanbul ignore next */
62
  if (document.readyState === 'loading') {
63
    return null;
64
  }
65
  const div1 = document.createElement('div');
2✔
66
  const div2 = document.createElement('div');
2✔
67
  div1.style.width =
2✔
68
    div2.style.width =
69
    div1.style.height =
70
    div2.style.height =
71
      '100px';
72
  div1.style.overflow = 'scroll';
2✔
73
  div2.style.overflow = 'hidden';
2✔
74
  document.body.appendChild(div1);
2✔
75
  document.body.appendChild(div2);
2✔
76
  scrollbarWidth = Math.abs(div1.scrollHeight - div2.scrollHeight);
2✔
77
  document.body.removeChild(div1);
2✔
78
  document.body.removeChild(div2);
2✔
79
  // save new screen size
80
  savedScreenSize = screenSize;
2✔
81
  return scrollbarWidth;
2✔
82
}
83

84
export function on(element, event, handler) {
85
  element.addEventListener(event, handler);
877✔
86
}
87

88
export function off(element, event, handler) {
89
  element.removeEventListener(event, handler);
1,425✔
90
}
91

92
export function isElement(el) {
93
  return el && el.nodeType === Node.ELEMENT_NODE;
2,023✔
94
}
95

96
export function removeFromDom(el) {
97
  isElement(el) && isElement(el.parentNode) && el.parentNode.removeChild(el);
399✔
98
}
99

100
export function addClass(el, className) {
101
  if (!isElement(el)) {
347✔
102
    return;
1✔
103
  }
104
  el.classList.add(className);
346✔
105
}
106

107
export function removeClass(el, className) {
108
  if (!isElement(el)) {
332✔
109
    return;
1✔
110
  }
111
  el.classList.remove(className);
331✔
112
}
113

114
export function hasClass(el, className) {
115
  if (!isElement(el)) {
312✔
116
    return false;
117✔
117
  }
118
  return el.classList.contains(className);
195✔
119
}
120

121
export function setDropdownPosition(dropdown, trigger, options = {}) {
×
122
  const doc = document.documentElement;
7✔
123
  const containerScrollLeft =
124
    (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
7✔
125
  const containerScrollTop =
126
    (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
7✔
127
  const rect = trigger.getBoundingClientRect();
7✔
128
  const dropdownRect = dropdown.getBoundingClientRect();
7✔
129
  dropdown.style.right = 'auto';
7✔
130
  dropdown.style.bottom = 'auto';
7✔
131
  if (options.menuRight) {
7✔
132
    const left =
133
      containerScrollLeft + rect.left + rect.width - dropdownRect.width;
3✔
134
    dropdown.style.left = left < 0 ? 0 : left + 'px';
3✔
135
  } else {
136
    dropdown.style.left = containerScrollLeft + rect.left + 'px';
4✔
137
  }
138
  if (options.dropup) {
7✔
139
    dropdown.style.top =
1✔
140
      containerScrollTop + rect.top - dropdownRect.height - 4 + 'px';
141
  } else {
142
    dropdown.style.top = containerScrollTop + rect.top + rect.height + 'px';
6✔
143
  }
144
}
145

146
export function isAvailableAtPosition(trigger, popup, placement) {
147
  const triggerRect = trigger.getBoundingClientRect();
48✔
148
  const popupRect = popup.getBoundingClientRect();
48✔
149
  const viewPortSize = getViewportSize();
48✔
150
  let top = true;
48✔
151
  let right = true;
48✔
152
  let bottom = true;
48✔
153
  let left = true;
48✔
154
  switch (placement) {
48✔
155
    case PLACEMENTS.TOP:
156
      top = triggerRect.top >= popupRect.height;
42✔
157
      left = triggerRect.left + triggerRect.width / 2 >= popupRect.width / 2;
42✔
158
      right =
42✔
159
        triggerRect.right - triggerRect.width / 2 + popupRect.width / 2 <=
160
        viewPortSize.width;
161
      break;
42✔
162
    case PLACEMENTS.BOTTOM:
163
      bottom = triggerRect.bottom + popupRect.height <= viewPortSize.height;
2✔
164
      left = triggerRect.left + triggerRect.width / 2 >= popupRect.width / 2;
2✔
165
      right =
2✔
166
        triggerRect.right - triggerRect.width / 2 + popupRect.width / 2 <=
167
        viewPortSize.width;
168
      break;
2✔
169
    case PLACEMENTS.RIGHT:
170
      right = triggerRect.right + popupRect.width <= viewPortSize.width;
2✔
171
      top = triggerRect.top + triggerRect.height / 2 >= popupRect.height / 2;
2✔
172
      bottom =
2✔
173
        triggerRect.bottom - triggerRect.height / 2 + popupRect.height / 2 <=
174
        viewPortSize.height;
175
      break;
2✔
176
    case PLACEMENTS.LEFT:
177
      left = triggerRect.left >= popupRect.width;
2✔
178
      top = triggerRect.top + triggerRect.height / 2 >= popupRect.height / 2;
2✔
179
      bottom =
2✔
180
        triggerRect.bottom - triggerRect.height / 2 + popupRect.height / 2 <=
181
        viewPortSize.height;
182
      break;
2✔
183
  }
184
  return top && right && bottom && left;
48✔
185
}
186

187
export function setTooltipPosition(
188
  tooltip,
189
  trigger,
190
  placement,
191
  auto,
192
  appendTo,
193
  positionBy,
194
  viewport
195
) {
196
  if (!isElement(tooltip) || !isElement(trigger)) {
48!
197
    return;
×
198
  }
199
  const isPopover =
200
    tooltip && tooltip.className && tooltip.className.indexOf('popover') >= 0;
48✔
201
  let containerScrollTop;
202
  let containerScrollLeft;
203
  if (!isExist(appendTo) || appendTo === 'body' || positionBy === 'body') {
48✔
204
    const doc = document.documentElement;
44✔
205
    containerScrollLeft =
44✔
206
      (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
176✔
207
    containerScrollTop =
44✔
208
      (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
176✔
209
  } else {
210
    const container = getElementBySelectorOrRef(positionBy || appendTo);
4✔
211
    containerScrollLeft = container.scrollLeft;
4✔
212
    containerScrollTop = container.scrollTop;
4✔
213
  }
214
  // auto adjust placement
215
  if (auto) {
48!
216
    // Try: right -> bottom -> left -> top
217
    // Cause the default placement is top
218
    const placements = [
48✔
219
      PLACEMENTS.RIGHT,
220
      PLACEMENTS.BOTTOM,
221
      PLACEMENTS.LEFT,
222
      PLACEMENTS.TOP,
223
    ];
224
    // The class switch helper function
225
    const changePlacementClass = (placement) => {
48✔
226
      // console.log(placement)
227
      placements.forEach((placement) => {
×
228
        removeClass(tooltip, placement);
×
229
      });
230
      addClass(tooltip, placement);
×
231
    };
232
    // No need to adjust if the default placement fits
233
    if (!isAvailableAtPosition(trigger, tooltip, placement)) {
48!
234
      for (let i = 0, l = placements.length; i < l; i++) {
×
235
        // Re-assign placement class
236
        changePlacementClass(placements[i]);
×
237
        // Break if new placement fits
238
        if (isAvailableAtPosition(trigger, tooltip, placements[i])) {
×
239
          placement = placements[i];
×
240
          break;
×
241
        }
242
      }
243
      changePlacementClass(placement);
×
244
    }
245
  }
246
  // fix left and top for tooltip
247
  const rect = trigger.getBoundingClientRect();
48✔
248
  const tooltipRect = tooltip.getBoundingClientRect();
48✔
249
  let top;
250
  let left;
251
  if (placement === PLACEMENTS.BOTTOM) {
48✔
252
    top = containerScrollTop + rect.top + rect.height;
2✔
253
    left =
2✔
254
      containerScrollLeft + rect.left + rect.width / 2 - tooltipRect.width / 2;
255
  } else if (placement === PLACEMENTS.LEFT) {
46✔
256
    top =
2✔
257
      containerScrollTop + rect.top + rect.height / 2 - tooltipRect.height / 2;
258
    left = containerScrollLeft + rect.left - tooltipRect.width;
2✔
259
  } else if (placement === PLACEMENTS.RIGHT) {
44✔
260
    top =
2✔
261
      containerScrollTop + rect.top + rect.height / 2 - tooltipRect.height / 2;
262
    // https://github.com/uiv-lib/uiv/issues/272
263
    // add 1px to fix above issue
264
    left = containerScrollLeft + rect.left + rect.width + 1;
2✔
265
  } else {
266
    top = containerScrollTop + rect.top - tooltipRect.height;
42✔
267
    left =
42✔
268
      containerScrollLeft + rect.left + rect.width / 2 - tooltipRect.width / 2;
269
  }
270
  let viewportEl;
271
  // viewport option
272
  if (isString(viewport)) {
48!
273
    viewportEl = document.querySelector(viewport);
×
274
  } else if (isFunction(viewport)) {
48!
275
    viewportEl = viewport(trigger);
×
276
  }
277
  if (isElement(viewportEl)) {
48!
278
    const popoverFix = isPopover ? 11 : 0;
×
279
    const viewportReact = viewportEl.getBoundingClientRect();
×
280
    const viewportTop = containerScrollTop + viewportReact.top;
×
281
    const viewportLeft = containerScrollLeft + viewportReact.left;
×
282
    const viewportBottom = viewportTop + viewportReact.height;
×
283
    const viewportRight = viewportLeft + viewportReact.width;
×
284
    // fix top
285
    if (top < viewportTop) {
×
286
      top = viewportTop;
×
287
    } else if (top + tooltipRect.height > viewportBottom) {
×
288
      top = viewportBottom - tooltipRect.height;
×
289
    }
290
    // fix left
291
    if (left < viewportLeft) {
×
292
      left = viewportLeft;
×
293
    } else if (left + tooltipRect.width > viewportRight) {
×
294
      left = viewportRight - tooltipRect.width;
×
295
    }
296
    // fix for popover pointer
297
    if (placement === PLACEMENTS.BOTTOM) {
×
298
      top -= popoverFix;
×
299
    } else if (placement === PLACEMENTS.LEFT) {
×
300
      left += popoverFix;
×
301
    } else if (placement === PLACEMENTS.RIGHT) {
×
302
      left -= popoverFix;
×
303
    } else {
304
      top += popoverFix;
×
305
    }
306
  }
307
  // set position finally
308
  tooltip.style.top = `${top}px`;
48✔
309
  tooltip.style.left = `${left}px`;
48✔
310
}
311

312
export function hasScrollbar(el) {
313
  const SCROLL = 'scroll';
108✔
314
  const hasVScroll = el.scrollHeight > el.clientHeight;
108✔
315
  const style = getComputedStyle(el);
108✔
316
  return hasVScroll || style.overflow === SCROLL || style.overflowY === SCROLL;
108✔
317
}
318

319
export function toggleBodyOverflow(enable) {
320
  const MODAL_OPEN = 'modal-open';
136✔
321
  const FIXED_CONTENT = '.navbar-fixed-top, .navbar-fixed-bottom';
136✔
322
  const body = document.body;
136✔
323
  if (enable) {
136✔
324
    removeClass(body, MODAL_OPEN);
82✔
325
    body.style.paddingRight = null;
82✔
326
    [...document.querySelectorAll(FIXED_CONTENT)].forEach((node) => {
82✔
327
      node.style.paddingRight = null;
2✔
328
    });
329
  } else {
330
    const documentHasScrollbar =
331
      hasScrollbar(document.documentElement) || hasScrollbar(document.body);
54✔
332
    if (documentHasScrollbar) {
54✔
333
      const scrollbarWidth = getScrollbarWidth();
2✔
334
      body.style.paddingRight = `${scrollbarWidth}px`;
2✔
335
      [...document.querySelectorAll(FIXED_CONTENT)].forEach((node) => {
2✔
336
        node.style.paddingRight = `${scrollbarWidth}px`;
2✔
337
      });
338
    }
339
    addClass(body, MODAL_OPEN);
54✔
340
  }
341
}
342

343
export function getClosest(el, selector) {
344
  if (!isElement(el)) {
6✔
345
    return null;
1✔
346
  }
347
  return el.closest(selector);
5✔
348
}
349

350
export function getParents(el, selector, until = null) {
30✔
351
  const parents = [];
114✔
352
  let parent = el.parentElement;
114✔
353
  while (parent) {
114✔
354
    if (parent.matches(selector)) {
1,052✔
355
      parents.push(parent);
43✔
356
    } else if (until && (until === parent || parent.matches(until))) {
1,009✔
357
      break;
12✔
358
    }
359
    parent = parent.parentElement;
1,040✔
360
  }
361
  return parents;
114✔
362
}
363

364
export function focus(el) {
365
  if (!isElement(el)) {
9✔
366
    return;
1✔
367
  }
368
  if (!el.getAttribute('tabindex')) {
8✔
369
    el.setAttribute('tabindex', '-1');
6✔
370
  }
371
  el.focus();
8✔
372
}
373

374
const MODAL_BACKDROP = 'modal-backdrop';
27✔
375

376
export function getOpenModals() {
377
  return document.querySelectorAll(`.${MODAL_BACKDROP}`);
189✔
378
}
379

380
export function getOpenModalNum() {
381
  return getOpenModals().length;
182✔
382
}
383

384
export function getElementBySelectorOrRef(q) {
385
  if (isString(q)) {
131✔
386
    // is selector
387
    return document.querySelector(q);
65✔
388
  } else if (isElement(q)) {
66✔
389
    // is element
390
    return q;
59✔
391
  } else if (isElement(q.$el)) {
7✔
392
    // is component
393
    return q.$el;
5✔
394
  } else {
395
    return null;
2✔
396
  }
397
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc