mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 12:56:48 +00:00
241 lines
8.3 KiB
TypeScript
241 lines
8.3 KiB
TypeScript
import type { Editor } from '@tiptap/vue-3'
|
|
import { ncIsArray } from 'nocodb-sdk'
|
|
|
|
// refer - https://stackoverflow.com/a/11752084
|
|
export const isMac = () => /Mac/i.test(navigator.platform)
|
|
export const isDrawerExist = () => document.querySelector('.ant-drawer-open')
|
|
export const isLinkDropdownExist = () => document.querySelector('.nc-links-dropdown.active')
|
|
export const isDrawerOrModalExist = () => document.querySelector('.ant-modal.active, .ant-drawer-open')
|
|
export const isExpandedFormOpenExist = () => document.querySelector('.nc-drawer-expanded-form.active')
|
|
export const isNestedExpandedFormOpenExist = () => document.querySelectorAll('.nc-drawer-expanded-form.active')?.length > 1
|
|
export const isExpandedCellInputExist = () => document.querySelector('.expanded-cell-input')
|
|
export const isExtensionPaneActive = () => document.querySelector('.nc-extension-pane')
|
|
export const isGeneralOverlayActive = () => document.querySelector('.nc-general-overlay')
|
|
export const isSelectActive = () => {
|
|
const els = document.querySelectorAll<HTMLElement>('.ant-select-dropdown')
|
|
return Array.from(els).some((el) => {
|
|
const style = window.getComputedStyle(el)
|
|
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
|
})
|
|
}
|
|
|
|
export const isViewSearchActive = () => document.querySelector('.nc-view-search-data') === document.activeElement
|
|
export const isCreateViewActive = () => document.querySelector('.nc-view-create-modal')
|
|
export const isActiveElementInsideExtension = () =>
|
|
['.extension-modal', '.nc-extension-pane', '.nc-modal-extension-market', '.nc-modal-share-collaborate'].some((selector) =>
|
|
document.querySelector(selector)?.contains(document.activeElement),
|
|
)
|
|
export const isActiveElementInsideScriptPane = () => document.querySelector('.nc-action-pane')?.contains(document.activeElement)
|
|
export const isTiptapDropdownExistInsideEditor = () => {
|
|
return document.querySelector('.tippy-box')
|
|
}
|
|
|
|
export const ncIsIframe = () => window.self !== window.top
|
|
|
|
export const isSidebarNodeRenameActive = () => document.querySelector('input.animate-sidebar-node-input-padding')
|
|
export function hasAncestorWithClass(element: HTMLElement, className: string | Array<string>): boolean {
|
|
const classNames = ncIsArray(className) ? className : [className]
|
|
|
|
return classNames.some((c) => !!element.closest(`.${c}`))
|
|
}
|
|
export const cmdKActive = () => document.querySelector('.cmdk-modal-active')
|
|
export const isCmdJActive = () => document.querySelector('.DocSearch--active')
|
|
export const isActiveInputElementExist = (e?: Event) => {
|
|
const activeElement = document.activeElement
|
|
const target = e?.target
|
|
|
|
// A rich text editor is a div with the contenteditable attribute set to true.
|
|
return (
|
|
activeElement instanceof HTMLInputElement ||
|
|
activeElement instanceof HTMLTextAreaElement ||
|
|
(activeElement instanceof HTMLElement && activeElement.isContentEditable) ||
|
|
target instanceof HTMLInputElement ||
|
|
target instanceof HTMLTextAreaElement ||
|
|
(target instanceof HTMLElement && target.isContentEditable)
|
|
)
|
|
}
|
|
export const isActiveButtonOrLinkElementExist = (e?: Event) => {
|
|
const activeElement = document.activeElement
|
|
const target = e?.target
|
|
|
|
// A rich text editor is a div with the contenteditable attribute set to true.
|
|
return (
|
|
activeElement instanceof HTMLButtonElement ||
|
|
activeElement instanceof HTMLAnchorElement ||
|
|
target instanceof HTMLButtonElement ||
|
|
target instanceof HTMLAnchorElement
|
|
)
|
|
}
|
|
|
|
export const isNcDropdownOpen = () => document.querySelector('.nc-dropdown.active')
|
|
export const isDropdownActive = () => document.querySelector('.nc-dropdown')
|
|
|
|
export const isFieldEditOrAddDropdownOpen = () => document.querySelector('.nc-dropdown-edit-column.active')
|
|
export const getScrollbarWidth = () => {
|
|
const outer = document.createElement('div')
|
|
outer.style.visibility = 'hidden'
|
|
outer.style.width = '100px'
|
|
document.body.appendChild(outer)
|
|
|
|
const widthNoScroll = outer.offsetWidth
|
|
outer.style.overflow = 'scroll'
|
|
|
|
const inner = document.createElement('div')
|
|
inner.style.width = '100%'
|
|
outer.appendChild(inner)
|
|
|
|
const widthWithScroll = inner.offsetWidth
|
|
outer?.parentNode?.removeChild(outer)
|
|
return widthNoScroll - widthWithScroll
|
|
}
|
|
|
|
export function getElementAtMouse<T>(cssSelector: string, { clientX, clientY }: { clientX: number; clientY: number }) {
|
|
return document.elementsFromPoint(clientX, clientY).find((el) => el.matches(cssSelector)) as T | undefined
|
|
}
|
|
|
|
export function forcedNextTick(cb: () => void) {
|
|
// See https://github.com/vuejs/vue/issues/9200
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
cb()
|
|
})
|
|
})
|
|
}
|
|
|
|
export function isSinglePrintableKey(key: string) {
|
|
// handles other languages as well which key.length === 1 might not
|
|
return [...key].length === 1
|
|
}
|
|
|
|
export const isMousePointerType = (event: Event) => {
|
|
return (
|
|
// PointerEvent style with mouse
|
|
('pointerType' in event && (event as PointerEvent).pointerType === 'mouse') ||
|
|
// Safari fallback to MouseEvent
|
|
event instanceof MouseEvent
|
|
)
|
|
}
|
|
|
|
export const isTouchEvent = (event: Event | TouchEvent) => !isMousePointerType(event)
|
|
|
|
export const focusInputEl = (querySelector: string, target?: HTMLElement) => {
|
|
if (typeof window === 'undefined') return
|
|
|
|
querySelector = querySelector ? `${querySelector} ` : ''
|
|
|
|
const targetEl = target || document
|
|
const inputEl =
|
|
(targetEl.querySelector(`${querySelector}input`) as HTMLInputElement) ||
|
|
(targetEl.querySelector(`${querySelector}textarea`) as HTMLTextAreaElement) ||
|
|
(targetEl.querySelector(`${querySelector}[contenteditable="true"]`) as HTMLElement) ||
|
|
(targetEl.querySelector(`${querySelector}[tabindex="0"]`) as HTMLElement)
|
|
|
|
if (inputEl) {
|
|
inputEl?.select?.()
|
|
inputEl?.focus?.()
|
|
}
|
|
|
|
return inputEl
|
|
}
|
|
|
|
export const isExpandCellKey = (event: Event) => {
|
|
if (event instanceof KeyboardEvent) {
|
|
return event.key === ' ' && event.shiftKey
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Check if an element is line-clamped
|
|
*
|
|
* **Note:**
|
|
* The `Range#getBoundingClientRect()` technique works best when text is not deeply nested.
|
|
* This technique has performance overhead — avoid using it on large lists.
|
|
*
|
|
* @param el - The element to check
|
|
* @returns True if the element is line-clamped, false otherwise
|
|
*/
|
|
export const isLineClamped = (el: HTMLElement): boolean => {
|
|
if (!el) return false
|
|
|
|
const range = document.createRange()
|
|
range.selectNodeContents(el)
|
|
|
|
const fullHeight = range.getBoundingClientRect().height
|
|
const actualHeight = el.getBoundingClientRect().height
|
|
|
|
return fullHeight > actualHeight
|
|
}
|
|
|
|
export const handleOnEscRichTextEditor = (event: KeyboardEvent, editor?: Editor) => {
|
|
if (isTiptapDropdownExistInsideEditor()) {
|
|
event.stopPropagation()
|
|
|
|
if (editor && !editor.state.selection.empty) {
|
|
const pos = editor.state.selection.to
|
|
editor.commands.setTextSelection(pos)
|
|
editor.commands.focus()
|
|
}
|
|
}
|
|
}
|
|
|
|
export const estimateTagWidth = ({
|
|
text,
|
|
fontSize = 14,
|
|
fontWeight = 600,
|
|
paddingX = 16, // left + right padding
|
|
iconWidth = 0, // icon width (if you have icon)
|
|
border = 2,
|
|
}: {
|
|
text: string
|
|
fontSize?: number
|
|
fontWeight?: number
|
|
paddingX?: number
|
|
iconWidth?: number
|
|
border?: number
|
|
}) => {
|
|
// Dummy average char width per font-weight/font-size
|
|
const avgCharWidth = fontWeight >= 600 ? fontSize * 0.6 : fontSize * 0.5
|
|
|
|
const textWidth = text.length * avgCharWidth
|
|
|
|
const totalWidth = textWidth + paddingX + iconWidth + border
|
|
|
|
return totalWidth
|
|
}
|
|
|
|
/**
|
|
* Remove query params from the URL
|
|
* @param keysToRemove - The keys to remove from the URL
|
|
*/
|
|
export const removeQueryParamsFromURL = (keysToRemove: string[]) => {
|
|
const url = new URL(window.location.href)
|
|
keysToRemove.forEach((key) => url.searchParams.delete(key))
|
|
window.history.replaceState({}, '', url.toString())
|
|
}
|
|
|
|
// Feature detection.
|
|
export const supportsKeyboardLock = 'keyboard' in navigator && navigator.keyboard && 'lock' in (navigator.keyboard as any)
|
|
|
|
export const openContactSalesEmail = (email: string = 'support@nocodb.com') => {
|
|
const a = document.createElement('a')
|
|
a.href = `mailto:${email}`
|
|
a.target = '_blank'
|
|
a.click()
|
|
}
|
|
|
|
export const getValidSlotName = (name: string, prefix?: string, suffix?: string): string => {
|
|
let slotName = name.replace(/\./g, '__')
|
|
|
|
if (prefix) {
|
|
slotName = `${prefix}-${slotName}`
|
|
}
|
|
|
|
if (suffix) {
|
|
slotName = `${slotName}-${suffix}`
|
|
}
|
|
|
|
return slotName
|
|
}
|