UNPKG

uiv

Version:

Bootstrap 3 components implemented by Vue 2.

416 lines (386 loc) 13.2 kB
import { isExist, isString, isFunction } from './object.utils' import { nodeListToArray } from './array.utils' export const EVENTS = { MOUSE_ENTER: 'mouseenter', MOUSE_LEAVE: 'mouseleave', MOUSE_DOWN: 'mousedown', MOUSE_UP: 'mouseup', FOCUS: 'focus', BLUR: 'blur', CLICK: 'click', INPUT: 'input', KEY_DOWN: 'keydown', KEY_UP: 'keyup', KEY_PRESS: 'keypress', RESIZE: 'resize', SCROLL: 'scroll', TOUCH_START: 'touchstart', TOUCH_END: 'touchend' } export const TRIGGERS = { CLICK: 'click', HOVER: 'hover', FOCUS: 'focus', HOVER_FOCUS: 'hover-focus', OUTSIDE_CLICK: 'outside-click', MANUAL: 'manual' } export const PLACEMENTS = { TOP: 'top', RIGHT: 'right', BOTTOM: 'bottom', LEFT: 'left' } export function isIE11 () { /* istanbul ignore next */ return !!window.MSInputMethodContext && !!document.documentMode } export function isIE10 () { return window.navigator.appVersion.indexOf('MSIE 10') !== -1 } export function getComputedStyle (el) { return window.getComputedStyle(el) } export function getViewportSize () { /* istanbul ignore next */ const width = Math.max(document.documentElement.clientWidth, window.innerWidth) || 0 /* istanbul ignore next */ const height = Math.max(document.documentElement.clientHeight, window.innerHeight) || 0 return { width, height } } let scrollbarWidth = null let savedScreenSize = null export function getScrollbarWidth (recalculate = false) { const screenSize = getViewportSize() // return directly when already calculated & not force recalculate & screen size not changed if (scrollbarWidth !== null && !recalculate && screenSize.height === savedScreenSize.height && screenSize.width === savedScreenSize.width) { return scrollbarWidth } /* istanbul ignore next */ if (document.readyState === 'loading') { return null } const div1 = document.createElement('div') const div2 = document.createElement('div') div1.style.width = div2.style.width = div1.style.height = div2.style.height = '100px' div1.style.overflow = 'scroll' div2.style.overflow = 'hidden' document.body.appendChild(div1) document.body.appendChild(div2) scrollbarWidth = Math.abs(div1.scrollHeight - div2.scrollHeight) document.body.removeChild(div1) document.body.removeChild(div2) // save new screen size savedScreenSize = screenSize return scrollbarWidth } export function on (element, event, handler) { /* istanbul ignore next */ element.addEventListener(event, handler) } export function off (element, event, handler) { /* istanbul ignore next */ element.removeEventListener(event, handler) } export function isElement (el) { return el && el.nodeType === Node.ELEMENT_NODE } export function removeFromDom (el) { isElement(el) && isElement(el.parentNode) && el.parentNode.removeChild(el) } export function ensureElementMatchesFunction () { /* istanbul ignore next */ if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function (s) { const matches = (this.document || this.ownerDocument).querySelectorAll(s) let i = matches.length // eslint-disable-next-line no-empty while (--i >= 0 && matches.item(i) !== this) {} return i > -1 } } } export function addClass (el, className) { if (!isElement(el)) { return } if (el.className) { const classes = el.className.split(' ') if (classes.indexOf(className) < 0) { classes.push(className) el.className = classes.join(' ') } } else { el.className = className } } export function removeClass (el, className) { if (!isElement(el)) { return } if (el.className) { const classes = el.className.split(' ') const newClasses = [] for (let i = 0, l = classes.length; i < l; i++) { if (classes[i] !== className) { newClasses.push(classes[i]) } } el.className = newClasses.join(' ') } } export function hasClass (el, className) { if (!isElement(el)) { return false } const classes = el.className.split(' ') for (let i = 0, l = classes.length; i < l; i++) { if (classes[i] === className) { return true } } return false } export function setDropdownPosition (dropdown, trigger, options = {}) { const doc = document.documentElement const containerScrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0) const containerScrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) const rect = trigger.getBoundingClientRect() const dropdownRect = dropdown.getBoundingClientRect() dropdown.style.right = 'auto' dropdown.style.bottom = 'auto' if (options.menuRight) { dropdown.style.left = containerScrollLeft + rect.left + rect.width - dropdownRect.width + 'px' } else { dropdown.style.left = containerScrollLeft + rect.left + 'px' } if (options.dropup) { dropdown.style.top = containerScrollTop + rect.top - dropdownRect.height - 4 + 'px' } else { dropdown.style.top = containerScrollTop + rect.top + rect.height + 'px' } } export function isAvailableAtPosition (trigger, popup, placement) { const triggerRect = trigger.getBoundingClientRect() const popupRect = popup.getBoundingClientRect() const viewPortSize = getViewportSize() let top = true let right = true let bottom = true let left = true switch (placement) { case PLACEMENTS.TOP: top = triggerRect.top >= popupRect.height left = triggerRect.left + triggerRect.width / 2 >= popupRect.width / 2 right = triggerRect.right - triggerRect.width / 2 + popupRect.width / 2 <= viewPortSize.width break case PLACEMENTS.BOTTOM: bottom = triggerRect.bottom + popupRect.height <= viewPortSize.height left = triggerRect.left + triggerRect.width / 2 >= popupRect.width / 2 right = triggerRect.right - triggerRect.width / 2 + popupRect.width / 2 <= viewPortSize.width break case PLACEMENTS.RIGHT: right = triggerRect.right + popupRect.width <= viewPortSize.width top = triggerRect.top + triggerRect.height / 2 >= popupRect.height / 2 bottom = triggerRect.bottom - triggerRect.height / 2 + popupRect.height / 2 <= viewPortSize.height break case PLACEMENTS.LEFT: left = triggerRect.left >= popupRect.width top = triggerRect.top + triggerRect.height / 2 >= popupRect.height / 2 bottom = triggerRect.bottom - triggerRect.height / 2 + popupRect.height / 2 <= viewPortSize.height break } return top && right && bottom && left } export function setTooltipPosition (tooltip, trigger, placement, auto, appendTo, positionBy, viewport) { if (!isElement(tooltip) || !isElement(trigger)) { return } const isPopover = tooltip && tooltip.className && tooltip.className.indexOf('popover') >= 0 let containerScrollTop let containerScrollLeft if (!isExist(appendTo) || appendTo === 'body' || positionBy === 'body') { const doc = document.documentElement containerScrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0) containerScrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) } else { const container = getElementBySelectorOrRef(positionBy || appendTo) containerScrollLeft = container.scrollLeft containerScrollTop = container.scrollTop } // auto adjust placement if (auto) { // Try: right -> bottom -> left -> top // Cause the default placement is top const placements = [PLACEMENTS.RIGHT, PLACEMENTS.BOTTOM, PLACEMENTS.LEFT, PLACEMENTS.TOP] // The class switch helper function const changePlacementClass = (placement) => { // console.log(placement) placements.forEach(placement => { removeClass(tooltip, placement) }) addClass(tooltip, placement) } // No need to adjust if the default placement fits if (!isAvailableAtPosition(trigger, tooltip, placement)) { for (let i = 0, l = placements.length; i < l; i++) { // Re-assign placement class changePlacementClass(placements[i]) // Break if new placement fits if (isAvailableAtPosition(trigger, tooltip, placements[i])) { placement = placements[i] break } } changePlacementClass(placement) } } // fix left and top for tooltip const rect = trigger.getBoundingClientRect() const tooltipRect = tooltip.getBoundingClientRect() let top let left if (placement === PLACEMENTS.BOTTOM) { top = containerScrollTop + rect.top + rect.height left = containerScrollLeft + rect.left + rect.width / 2 - tooltipRect.width / 2 } else if (placement === PLACEMENTS.LEFT) { top = containerScrollTop + rect.top + rect.height / 2 - tooltipRect.height / 2 left = containerScrollLeft + rect.left - tooltipRect.width } else if (placement === PLACEMENTS.RIGHT) { top = containerScrollTop + rect.top + rect.height / 2 - tooltipRect.height / 2 // https://github.com/uiv-lib/uiv/issues/272 // add 1px to fix above issue left = containerScrollLeft + rect.left + rect.width + 1 } else { top = containerScrollTop + rect.top - tooltipRect.height left = containerScrollLeft + rect.left + rect.width / 2 - tooltipRect.width / 2 } let viewportEl // viewport option if (isString(viewport)) { viewportEl = document.querySelector(viewport) } else if (isFunction(viewport)) { viewportEl = viewport(trigger) } if (isElement(viewportEl)) { const popoverFix = isPopover ? 11 : 0 const viewportReact = viewportEl.getBoundingClientRect() const viewportTop = containerScrollTop + viewportReact.top const viewportLeft = containerScrollLeft + viewportReact.left const viewportBottom = viewportTop + viewportReact.height const viewportRight = viewportLeft + viewportReact.width // fix top if (top < viewportTop) { top = viewportTop } else if (top + tooltipRect.height > viewportBottom) { top = viewportBottom - tooltipRect.height } // fix left if (left < viewportLeft) { left = viewportLeft } else if (left + tooltipRect.width > viewportRight) { left = viewportRight - tooltipRect.width } // fix for popover pointer if (placement === PLACEMENTS.BOTTOM) { top -= popoverFix } else if (placement === PLACEMENTS.LEFT) { left += popoverFix } else if (placement === PLACEMENTS.RIGHT) { left -= popoverFix } else { top += popoverFix } } // set position finally tooltip.style.top = `${top}px` tooltip.style.left = `${left}px` } export function hasScrollbar (el) { const SCROLL = 'scroll' const hasVScroll = el.scrollHeight > el.clientHeight const style = getComputedStyle(el) return hasVScroll || style.overflow === SCROLL || style.overflowY === SCROLL } export function toggleBodyOverflow (enable) { const MODAL_OPEN = 'modal-open' const FIXED_CONTENT = '.navbar-fixed-top, .navbar-fixed-bottom' const body = document.body if (enable) { removeClass(body, MODAL_OPEN) body.style.paddingRight = null nodeListToArray(document.querySelectorAll(FIXED_CONTENT)).forEach(node => { node.style.paddingRight = null }) } else { const browsersWithFloatingScrollbar = isIE10() || isIE11() const documentHasScrollbar = hasScrollbar(document.documentElement) || hasScrollbar(document.body) if (documentHasScrollbar && !browsersWithFloatingScrollbar) { const scrollbarWidth = getScrollbarWidth() body.style.paddingRight = `${scrollbarWidth}px` nodeListToArray(document.querySelectorAll(FIXED_CONTENT)).forEach(node => { node.style.paddingRight = `${scrollbarWidth}px` }) } addClass(body, MODAL_OPEN) } } export function getClosest (el, selector) { ensureElementMatchesFunction() let parent let _el = el while (_el) { parent = _el.parentElement if (parent && parent.matches(selector)) { return parent } _el = parent } return null } export function getParents (el, selector, until = null) { ensureElementMatchesFunction() const parents = [] let parent = el.parentElement while (parent) { if (parent.matches(selector)) { parents.push(parent) } else if (until && (until === parent || parent.matches(until))) { break } parent = parent.parentElement } return parents } export function focus (el) { if (!isElement(el)) { return } el.getAttribute('tabindex') ? null : el.setAttribute('tabindex', '-1') el.focus() } const MODAL_BACKDROP = 'modal-backdrop' export function getOpenModals () { return document.querySelectorAll(`.${MODAL_BACKDROP}`) } export function getOpenModalNum () { return getOpenModals().length } export function getElementBySelectorOrRef (q) { if (isString(q)) { // is selector return document.querySelector(q) } else if (isElement(q)) { // is element return q } else if (isElement(q.$el)) { // is component return q.$el } else { return null } }