UNPKG

uiv

Version:

Bootstrap 3 components implemented by Vue.

398 lines (371 loc) 11.7 kB
import { isExist, isString, isFunction } from './object.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 getComputedStyle(el) { return window.getComputedStyle(el); } export function getViewportSize() { const width = window.innerWidth || 0; const height = 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) { element.addEventListener(event, handler); } export function off(element, event, handler) { 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 addClass(el, className) { if (!isElement(el)) { return; } el.classList.add(className); } export function removeClass(el, className) { if (!isElement(el)) { return; } el.classList.remove(className); } export function hasClass(el, className) { if (!isElement(el)) { return false; } return el.classList.contains(className); } 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) { const left = containerScrollLeft + rect.left + rect.width - dropdownRect.width; dropdown.style.left = left < 0 ? 0 : left + '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; [...document.querySelectorAll(FIXED_CONTENT)].forEach((node) => { node.style.paddingRight = null; }); } else { const documentHasScrollbar = hasScrollbar(document.documentElement) || hasScrollbar(document.body); if (documentHasScrollbar) { const scrollbarWidth = getScrollbarWidth(); body.style.paddingRight = `${scrollbarWidth}px`; [...document.querySelectorAll(FIXED_CONTENT)].forEach((node) => { node.style.paddingRight = `${scrollbarWidth}px`; }); } addClass(body, MODAL_OPEN); } } export function getClosest(el, selector) { if (!isElement(el)) { return null; } return el.closest(selector); } export function getParents(el, selector, until = null) { 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; } if (!el.getAttribute('tabindex')) { 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; } }