refactor: adopt shared utility libraries
- Use VueUse for object URLs, timers, and clipboard state - Replace custom collection and equality helpers with es-toolkit - Use ofetch and safeDestr at remote API and JSON boundaries
This commit is contained in:
parent
f525c0f4fb
commit
b1e629e95f
2
bun.lock
2
bun.lock
|
|
@ -142,6 +142,7 @@
|
|||
"acorn": "^8.16.0",
|
||||
"canvaskit-wasm": "^0.40.0",
|
||||
"culori": "^4.0.2",
|
||||
"destr": "^2.0.5",
|
||||
"diff": "^8.0.3",
|
||||
"es-toolkit": "^1.46.1",
|
||||
"expr-eval": "^2.0.2",
|
||||
|
|
@ -150,6 +151,7 @@
|
|||
"fzstd": "^0.1.1",
|
||||
"jspdf": "^4.2.1",
|
||||
"nanoevents": "^9.1.0",
|
||||
"ofetch": "^1.5.1",
|
||||
"opentype.js": "^1.3.4",
|
||||
"sucrase": "^3.35.1",
|
||||
"svg2pdf.js": "^2.7.0",
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@
|
|||
"acorn": "^8.16.0",
|
||||
"canvaskit-wasm": "^0.40.0",
|
||||
"culori": "^4.0.2",
|
||||
"destr": "^2.0.5",
|
||||
"diff": "^8.0.3",
|
||||
"es-toolkit": "^1.46.1",
|
||||
"expr-eval": "^2.0.2",
|
||||
|
|
@ -182,6 +183,7 @@
|
|||
"fzstd": "^0.1.1",
|
||||
"jspdf": "^4.2.1",
|
||||
"nanoevents": "^9.1.0",
|
||||
"ofetch": "^1.5.1",
|
||||
"opentype.js": "^1.3.4",
|
||||
"sucrase": "^3.35.1",
|
||||
"svg2pdf.js": "^2.7.0",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Paragraph } from 'canvaskit-wasm'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
|
||||
import type { SceneNode, StyleRun } from '@open-pencil/scene-graph'
|
||||
import { copyStyleRuns } from '@open-pencil/scene-graph/copy'
|
||||
import type { JsonObject } from '@open-pencil/scene-graph/primitives'
|
||||
|
||||
export type TextEditSizeSnapshot = Partial<Pick<SceneNode, 'width' | 'height'>>
|
||||
|
||||
|
|
@ -92,22 +92,5 @@ function fillsEqual(
|
|||
b: NonNullable<StyleRun['style']['fills']>
|
||||
) {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((fill, index) => deepEqual(fill, b[index]))
|
||||
}
|
||||
|
||||
function deepEqual(a: unknown, b: unknown): boolean {
|
||||
if (Object.is(a, b)) return true
|
||||
if (typeof a !== typeof b) return false
|
||||
if (a === null || b === null) return false
|
||||
if (typeof a !== 'object' || typeof b !== 'object') return false
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
|
||||
return a.every((item, index) => deepEqual(item, b[index]))
|
||||
}
|
||||
const aRecord = a as JsonObject
|
||||
const bRecord = b as JsonObject
|
||||
const aKeys = Object.keys(aRecord)
|
||||
const bKeys = Object.keys(bRecord)
|
||||
if (aKeys.length !== bKeys.length) return false
|
||||
return aKeys.every((key) => Object.hasOwn(bRecord, key) && deepEqual(aRecord[key], bRecord[key]))
|
||||
return a.every((fill, index) => isEqual(fill, b[index]))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
import { ofetch } from 'ofetch'
|
||||
|
||||
import type { IconifyResponse, IconSearchResult } from './types'
|
||||
|
||||
const ICONIFY_API = 'https://api.iconify.design'
|
||||
const FETCH_TIMEOUT_MS = 10_000
|
||||
|
||||
function fetchWithTimeout(url: string): Promise<Response> {
|
||||
return fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
|
||||
}
|
||||
const iconifyApi = ofetch.create({
|
||||
baseURL: ICONIFY_API,
|
||||
retry: 0,
|
||||
timeout: FETCH_TIMEOUT_MS
|
||||
})
|
||||
|
||||
export async function fetchIconifyCollection(
|
||||
prefix: string,
|
||||
iconNames: string[]
|
||||
): Promise<IconifyResponse> {
|
||||
const url = `${ICONIFY_API}/${prefix}.json?icons=${iconNames.map(encodeURIComponent).join(',')}`
|
||||
const response = await fetchWithTimeout(url)
|
||||
const response = await iconifyApi.raw<IconifyResponse>(`/${prefix}.json`, {
|
||||
ignoreResponseError: true,
|
||||
query: { icons: iconNames.join(',') }
|
||||
})
|
||||
if (!response.ok) throw new Error(`Iconify API error: ${response.status} for prefix "${prefix}"`)
|
||||
return (await response.json()) as IconifyResponse
|
||||
return response._data as IconifyResponse
|
||||
}
|
||||
|
||||
export async function searchIconify(
|
||||
|
|
@ -24,18 +30,17 @@ export async function searchIconify(
|
|||
prefix?: string
|
||||
}
|
||||
): Promise<IconSearchResult> {
|
||||
const params = new URLSearchParams({ query })
|
||||
if (options?.limit) params.set('limit', String(options.limit))
|
||||
if (options?.prefix) params.set('prefix', options.prefix)
|
||||
|
||||
const response = await fetchWithTimeout(`${ICONIFY_API}/search?${params}`)
|
||||
const response = await iconifyApi.raw<IconSearchResult>('/search', {
|
||||
ignoreResponseError: true,
|
||||
query: { query, limit: options?.limit, prefix: options?.prefix }
|
||||
})
|
||||
if (!response.ok) throw new Error(`Iconify search error: ${response.status}`)
|
||||
const data = await response.json()
|
||||
const icons: string[] = data.icons ?? []
|
||||
|
||||
const data = response._data
|
||||
const limit = options?.limit ?? 5
|
||||
return {
|
||||
icons: icons.slice(0, limit),
|
||||
total: data.total ?? 0,
|
||||
collections: data.collections ?? {}
|
||||
icons: data?.icons.slice(0, limit) ?? [],
|
||||
total: data?.total ?? 0,
|
||||
collections: data?.collections ?? {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { orderBy, sortBy } from 'es-toolkit/array'
|
||||
import { meanBy } from 'es-toolkit/math'
|
||||
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
|
|
@ -88,8 +89,8 @@ export const analyzeClusters = defineTool({
|
|||
[...signatureMap.entries()]
|
||||
.filter(([, nodes]) => nodes.length >= minCount)
|
||||
.map(([signature, nodes]) => {
|
||||
const avgWidth = nodes.reduce((sum, node) => sum + node.width, 0) / nodes.length
|
||||
const avgHeight = nodes.reduce((sum, node) => sum + node.height, 0) / nodes.length
|
||||
const avgWidth = meanBy(nodes, (node) => node.width)
|
||||
const avgHeight = meanBy(nodes, (node) => node.height)
|
||||
const widths = nodes.map((node) => node.width)
|
||||
const heights = nodes.map((node) => node.height)
|
||||
const widthRange = Math.max(...widths) - Math.min(...widths)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { orderBy } from 'es-toolkit/array'
|
||||
import { sumBy } from 'es-toolkit/math'
|
||||
|
||||
import type { Color } from '@open-pencil/scene-graph/primitives'
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ export const analyzeColors = defineTool({
|
|||
if (cluster.length > 1) {
|
||||
clusters.push({
|
||||
colors: cluster.map((c) => c.hex),
|
||||
totalCount: cluster.reduce((sum, c) => sum + c.count, 0),
|
||||
totalCount: sumBy(cluster, (color) => color.count),
|
||||
suggestedHex: color.hex
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { safeDestr } from 'destr'
|
||||
import { createTwoFilesPatch } from 'diff'
|
||||
|
||||
import type { SceneNode } from '@open-pencil/scene-graph'
|
||||
|
|
@ -193,7 +194,7 @@ export const diffShow = defineTool({
|
|||
|
||||
let newProps: Record<string, unknown>
|
||||
try {
|
||||
newProps = JSON.parse(args.props)
|
||||
newProps = safeDestr<Record<string, unknown>>(args.props)
|
||||
} catch {
|
||||
return { error: 'Invalid JSON in props' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { safeDestr } from 'destr'
|
||||
|
||||
import { normalizeVectorNetwork, validateVectorNetwork } from '@open-pencil/scene-graph'
|
||||
import type { VectorNetwork } from '@open-pencil/scene-graph'
|
||||
|
||||
|
|
@ -26,7 +28,7 @@ export const createVector = defineTool({
|
|||
if (args.path) {
|
||||
let parsed: VectorNetwork
|
||||
try {
|
||||
parsed = JSON.parse(args.path) as VectorNetwork
|
||||
parsed = safeDestr<VectorNetwork>(args.path)
|
||||
} catch {
|
||||
return { error: 'Invalid JSON in path parameter' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { wcagLuminance } from 'culori'
|
||||
import { sumBy } from 'es-toolkit/math'
|
||||
|
||||
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
|
||||
import type { Color } from '@open-pencil/scene-graph/primitives'
|
||||
|
|
@ -70,7 +71,7 @@ function checkAlignmentIssues(ctx: LayoutContext): void {
|
|||
return Math.abs(dim - (isRow ? children[0].width : children[0].height)) < 2
|
||||
})
|
||||
if (allSameSize && node.primaryAxisAlign === 'MIN' && node.itemSpacing === 0) {
|
||||
const total = children.reduce((s, c) => s + (isRow ? c.width : c.height), 0)
|
||||
const total = sumBy(children, (child) => (isRow ? child.width : child.height))
|
||||
const pad = isRow ? node.paddingLeft + node.paddingRight : node.paddingTop + node.paddingBottom
|
||||
if (total < ((isRow ? node.width : node.height) - pad) * 0.7) {
|
||||
issues.push({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { ofetch } from 'ofetch'
|
||||
|
||||
export interface StockPhotoResult {
|
||||
url: string
|
||||
width: number
|
||||
|
|
@ -76,10 +78,17 @@ const pexelsProvider: StockPhotoProvider = {
|
|||
name: 'pexels',
|
||||
async search(query, { perPage, orientation, targetDim }) {
|
||||
if (!pexelsApiKey) throw new Error('Pexels API key not configured')
|
||||
const url = `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=${perPage}&orientation=${orientation}`
|
||||
const resp = await fetch(url, { headers: { Authorization: pexelsApiKey } })
|
||||
if (!resp.ok) throw new Error(`Pexels ${resp.status}`)
|
||||
const data = (await resp.json()) as { photos: PexelsPhoto[] }
|
||||
const response = await ofetch.raw<{ photos: PexelsPhoto[] }>(
|
||||
'https://api.pexels.com/v1/search',
|
||||
{
|
||||
headers: { Authorization: pexelsApiKey },
|
||||
ignoreResponseError: true,
|
||||
query: { query, per_page: perPage, orientation },
|
||||
retry: 0
|
||||
}
|
||||
)
|
||||
if (!response.ok) throw new Error(`Pexels ${response.status}`)
|
||||
const data = response._data as { photos: PexelsPhoto[] }
|
||||
return data.photos.map((photo) => ({
|
||||
url: pickPexelsSize(photo.src, targetDim),
|
||||
width: photo.width,
|
||||
|
|
@ -120,15 +129,20 @@ const unsplashProvider: StockPhotoProvider = {
|
|||
async search(query, { perPage, orientation }) {
|
||||
if (!unsplashAccessKey) throw new Error('Unsplash access key not configured')
|
||||
const orient = orientation === 'square' ? 'squarish' : orientation
|
||||
const url = `https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=${perPage}&orientation=${orient}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Client-ID ${unsplashAccessKey}`,
|
||||
'Accept-Version': 'v1'
|
||||
const response = await ofetch.raw<{ results: UnsplashPhoto[] }>(
|
||||
'https://api.unsplash.com/search/photos',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Client-ID ${unsplashAccessKey}`,
|
||||
'Accept-Version': 'v1'
|
||||
},
|
||||
ignoreResponseError: true,
|
||||
query: { query, per_page: perPage, orientation: orient },
|
||||
retry: 0
|
||||
}
|
||||
})
|
||||
if (!resp.ok) throw new Error(`Unsplash ${resp.status}`)
|
||||
const data = (await resp.json()) as { results: UnsplashPhoto[] }
|
||||
)
|
||||
if (!response.ok) throw new Error(`Unsplash ${response.status}`)
|
||||
const data = response._data as { results: UnsplashPhoto[] }
|
||||
return data.results.map((photo) => ({
|
||||
url: pickUnsplashSize(photo.urls, 1080),
|
||||
width: photo.width,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { safeDestr } from 'destr'
|
||||
|
||||
import type { PhotoRequest } from './apply'
|
||||
|
||||
export function parsePhotoRequests(value: unknown): PhotoRequest[] | { error: string } {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(String(value))
|
||||
parsed = safeDestr(String(value))
|
||||
} catch {
|
||||
return { error: 'Invalid JSON in requests' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { safeDestr } from 'destr'
|
||||
|
||||
import type { FigmaNodeProxy } from '#core/figma-api'
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
|
|
@ -103,7 +105,7 @@ export const batchUpdate = defineTool({
|
|||
execute: (figma, { operations }) => {
|
||||
let ops: BatchOp[]
|
||||
try {
|
||||
ops = JSON.parse(String(operations))
|
||||
ops = safeDestr<BatchOp[]>(String(operations))
|
||||
} catch {
|
||||
return { error: 'Invalid JSON in operations' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onScopeDispose, ref, watch } from 'vue'
|
||||
import { useObjectUrl } from '@vueuse/core'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
|
|
@ -38,7 +39,8 @@ const { panels, commands } = useI18n()
|
|||
const query = ref('')
|
||||
const detailsOpen = ref(false)
|
||||
const selectedAssetId = ref<string | null>(null)
|
||||
const previewUrl = ref<string | null>(null)
|
||||
const previewBlob = shallowRef<Blob | null>(null)
|
||||
const previewUrl = useObjectUrl(previewBlob)
|
||||
const previewLoading = ref(false)
|
||||
let previewRequestId = 0
|
||||
const insertButton = useButtonUI({ tone: 'ghost', size: 'iconSm' })
|
||||
|
|
@ -98,23 +100,21 @@ const selectedAsset = computed(
|
|||
)
|
||||
const selectedPreviewNodeId = computed(() => selectedAsset.value?.componentId ?? null)
|
||||
|
||||
function revokePreview() {
|
||||
if (!previewUrl.value) return
|
||||
URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = null
|
||||
function clearPreview() {
|
||||
previewBlob.value = null
|
||||
}
|
||||
|
||||
async function updatePreview() {
|
||||
const requestId = ++previewRequestId
|
||||
const nodeId = selectedPreviewNodeId.value
|
||||
if (!detailsOpen.value || !nodeId) {
|
||||
revokePreview()
|
||||
clearPreview()
|
||||
return
|
||||
}
|
||||
|
||||
const node = editor.getNode(nodeId)
|
||||
if (!node) {
|
||||
revokePreview()
|
||||
clearPreview()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -124,8 +124,7 @@ async function updatePreview() {
|
|||
const scale = Math.min(176 / maxSize, 2)
|
||||
const data = await editor.renderExportImage([nodeId], scale, 'PNG')
|
||||
if (requestId !== previewRequestId) return
|
||||
revokePreview()
|
||||
if (data) previewUrl.value = URL.createObjectURL(new Blob([data], { type: 'image/png' }))
|
||||
previewBlob.value = data ? new Blob([data], { type: 'image/png' }) : null
|
||||
} finally {
|
||||
if (requestId === previewRequestId) previewLoading.value = false
|
||||
}
|
||||
|
|
@ -135,8 +134,6 @@ watch([detailsOpen, selectedPreviewNodeId, () => editor.state.sceneVersion], upd
|
|||
flush: 'post'
|
||||
})
|
||||
|
||||
onScopeDispose(revokePreview)
|
||||
|
||||
function openDetails(asset: LocalAsset) {
|
||||
selectedAssetId.value = asset.id
|
||||
detailsOpen.value = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui'
|
||||
import { refAutoReset } from '@vueuse/core'
|
||||
import { refAutoReset, useClipboard } from '@vueuse/core'
|
||||
import { computed, markRaw, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { getAcpDebugText, clearAcpDebugLog, hasAcpDebugEntries } from '@/app/ai/acp/transport'
|
||||
|
|
@ -23,6 +23,7 @@ import type { JsonObject } from '@open-pencil/scene-graph/primitives'
|
|||
const IS_DEV = import.meta.env.DEV
|
||||
|
||||
const { isConfigured, ensureChat, resetChat } = useAIChat()
|
||||
const { copy } = useClipboard()
|
||||
const { dialogs } = useI18n()
|
||||
|
||||
const chat = ref<Chat<UIMessage> | null>(null)
|
||||
|
|
@ -112,7 +113,7 @@ async function handleCopyDebug() {
|
|||
async function handleCopyAcpLog() {
|
||||
const text = getAcpDebugText()
|
||||
if (!text) return
|
||||
await navigator.clipboard.writeText(text)
|
||||
await copy(text)
|
||||
acpLogCopied.value = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onScopeDispose } from 'vue'
|
||||
import { useObjectUrl } from '@vueuse/core'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import ExportScaleInput from '@/components/properties/ExportScaleInput.vue'
|
||||
|
|
@ -36,7 +37,8 @@ const FORMAT_OPTIONS: { value: ExportFormatId; label: string }[] = [
|
|||
{ value: 'pdf', label: 'PDF' }
|
||||
]
|
||||
|
||||
const previewUrl = ref<string | null>(null)
|
||||
const previewBlob = shallowRef<Blob | null>(null)
|
||||
const previewUrl = useObjectUrl(previewBlob)
|
||||
const showPreview = ref(false)
|
||||
const exporting = ref(false)
|
||||
|
||||
|
|
@ -76,8 +78,7 @@ async function updatePreview() {
|
|||
: editorStore.graph.getChildren(editorStore.state.currentPageId).map((n) => n.id)
|
||||
|
||||
if (ids.length === 0) {
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = null
|
||||
previewBlob.value = null
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -88,11 +89,7 @@ async function updatePreview() {
|
|||
}
|
||||
const scale = maxW > 0 ? Math.min(PREVIEW_WIDTH / maxW, 2) : 1
|
||||
const data = await editorStore.renderExportImage(ids, scale, 'PNG')
|
||||
if (data) {
|
||||
const prev = previewUrl.value
|
||||
previewUrl.value = URL.createObjectURL(new Blob([data], { type: 'image/png' }))
|
||||
if (prev) URL.revokeObjectURL(prev)
|
||||
}
|
||||
previewBlob.value = data ? new Blob([data], { type: 'image/png' }) : null
|
||||
}
|
||||
|
||||
const previewKey = computed(
|
||||
|
|
@ -106,10 +103,6 @@ const previewKey = computed(
|
|||
|
||||
watch(() => showPreview.value, updatePreview, { flush: 'post' })
|
||||
watch(previewKey, updatePreview, { flush: 'post' })
|
||||
|
||||
onScopeDispose(() => {
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useEventListener, useTimeoutFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useTooltipUI } from '@/components/ui/tooltip'
|
||||
|
||||
|
|
@ -26,7 +26,6 @@ const triggerRef = ref<HTMLElement>()
|
|||
const contentRef = ref<HTMLElement>()
|
||||
const open = ref(false)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
let openTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const canOpen = computed(() => Boolean(label) && !disabled)
|
||||
const contentStyle = computed(() => ({
|
||||
|
|
@ -34,11 +33,14 @@ const contentStyle = computed(() => ({
|
|||
top: `${position.value.y}px`
|
||||
}))
|
||||
|
||||
function clearOpenTimer() {
|
||||
if (!openTimer) return
|
||||
clearTimeout(openTimer)
|
||||
openTimer = undefined
|
||||
}
|
||||
const { start: startOpenTimer, stop: stopOpenTimer } = useTimeoutFn(
|
||||
() => {
|
||||
open.value = true
|
||||
void nextTick(refreshPosition)
|
||||
},
|
||||
TOOLTIP_OPEN_DELAY_MS,
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
function anchorElement() {
|
||||
const root = triggerRef.value
|
||||
|
|
@ -91,15 +93,12 @@ function refreshPosition() {
|
|||
|
||||
function show() {
|
||||
if (!canOpen.value) return
|
||||
clearOpenTimer()
|
||||
openTimer = setTimeout(() => {
|
||||
open.value = true
|
||||
void nextTick(refreshPosition)
|
||||
}, TOOLTIP_OPEN_DELAY_MS)
|
||||
stopOpenTimer()
|
||||
startOpenTimer()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clearOpenTimer()
|
||||
stopOpenTimer()
|
||||
open.value = false
|
||||
}
|
||||
|
||||
|
|
@ -140,8 +139,6 @@ useEventListener(document, 'click', hide, { capture: true })
|
|||
watch(canOpen, (value) => {
|
||||
if (!value) hide()
|
||||
})
|
||||
|
||||
onBeforeUnmount(hide)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
Loading…
Reference in a new issue