feat: Add loading state to PackInstallButton and improve UI (#5153)

This commit is contained in:
Jin Yi
2025-08-23 13:28:13 +09:00
committed by GitHub
parent 7d1659c04a
commit 6470869eb1
19 changed files with 260 additions and 159 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -16,6 +16,14 @@ const meta: Meta<typeof IconButton> = {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
onClick: { action: 'clicked' }
}
}

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" @click="onClick">
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<slot></slot>
</Button>
</template>
@@ -11,6 +11,7 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonTypeClasses,
getIconButtonSizeClasses
} from '@/types/buttonTypes'
@@ -22,6 +23,8 @@ interface IconButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'secondary',
border = false,
disabled = false,
class: className,
onClick
} = defineProps<IconButtonProps>()
@@ -29,7 +32,9 @@ const {
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} p-0`
const sizeClasses = getIconButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -28,6 +28,14 @@ const meta: Meta<typeof IconTextButton> = {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
iconPosition: {
control: { type: 'select' },
options: ['left', 'right']

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" @click="onClick">
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
@@ -13,6 +13,7 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
@@ -26,6 +27,8 @@ interface IconTextButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
iconPosition = 'left',
label,
@@ -35,7 +38,9 @@ const {
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -16,6 +16,14 @@ const meta: Meta<typeof TextButton> = {
options: ['sm', 'md'],
defaultValue: 'md'
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent'],

View File

@@ -1,5 +1,5 @@
<template>
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
<Button unstyled :class="buttonStyle" :disabled="disabled" @click="onClick">
<span>{{ label }}</span>
</Button>
</template>
@@ -11,6 +11,7 @@ import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
@@ -23,6 +24,8 @@ interface TextButtonProps extends BaseButtonProps {
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
label,
onClick
@@ -31,7 +34,9 @@ const {
const buttonStyle = computed(() => {
const baseClasses = getBaseButtonClasses()
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)

View File

@@ -33,10 +33,16 @@
</ListBox>
<div v-if="!isLegacyManager" class="flex justify-end py-3">
<PackInstallButton
:disabled="isLoading || !!error || missingNodePacks.length === 0"
size="md"
:disabled="!!error || missingNodePacks.length === 0"
:is-loading="isLoading"
:is-installing="isInstalling"
:node-packs="missingNodePacks"
variant="black"
:label="$t('manager.installAllMissingNodes')"
:label="
isLoading
? $t('manager.gettingInfo')
: $t('manager.installAllMissingNodes')
"
/>
<Button label="Open Manager" size="small" outlined @click="openManager" />
</div>
@@ -48,12 +54,16 @@ import ListBox from 'primevue/listbox'
import { computed, onMounted, ref } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useDialogService } from '@/services/dialogService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { MissingNodeType } from '@/types/comfy'
import { ManagerTab } from '@/types/comfyManagerTypes'
import PackInstallButton from './manager/button/PackInstallButton.vue'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
@@ -62,8 +72,17 @@ const props = defineProps<{
const { missingNodePacks, isLoading, error, missingCoreNodes } =
useMissingNodes()
const comfyManagerStore = useComfyManagerStore()
const isLegacyManager = ref(false)
// Check if any of the missing packs are currently being installed
const isInstalling = computed(() => {
if (!missingNodePacks.value?.length) return false
return missingNodePacks.value.some((pack) =>
comfyManagerStore.isPackInstalling(pack.id)
)
})
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return props.missingNodeTypes

View File

@@ -12,11 +12,10 @@
>
<i
v-if="isUpdateAvailable"
class="pi pi-arrow-circle-up text-blue-600"
style="font-size: 8px"
class="pi pi-arrow-circle-up text-blue-600 text-xs"
/>
<span>{{ installedVersion }}</span>
<i class="pi pi-chevron-right" style="font-size: 8px" />
<i class="pi pi-chevron-right text-xxs" />
</div>
<Popover

View File

@@ -1,53 +0,0 @@
<template>
<Button
outlined
class="!m-0 p-0 rounded-lg text-gray-900 dark-theme:text-gray-50"
:class="[
variant === 'black'
? 'bg-neutral-900 text-white border-neutral-900'
: 'border-neutral-700',
fullWidth ? 'w-full' : 'w-min-content'
]"
:disabled="loading"
v-bind="$attrs"
@click="onClick"
>
<span class="py-2 px-3 whitespace-nowrap">
<template v-if="loading">
{{ loadingMessage ?? $t('g.loading') }}
</template>
<template v-else>
{{ label }}
</template>
</span>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
const {
label,
loadingMessage,
fullWidth = false,
variant = 'default'
} = defineProps<{
label: string
loading?: boolean
loadingMessage?: string
fullWidth?: boolean
variant?: 'default' | 'black'
}>()
const emit = defineEmits<{
action: []
}>()
defineOptions({
inheritAttrs: false
})
const onClick = (): void => {
emit('action')
}
</script>

View File

@@ -1,25 +1,28 @@
<template>
<PackActionButton
<IconTextButton
v-bind="$attrs"
:label="
label ??
(nodePacks.length > 1 ? $t('manager.installSelected') : $t('g.install'))
"
:severity="variant === 'black' ? undefined : 'secondary'"
:variant="variant"
:loading="isInstalling"
:loading-message="$t('g.installing')"
@action="installAllPacks"
/>
type="transparent"
:label="computedLabel"
:border="true"
:size="size"
:disabled="isLoading || isInstalling"
@click="installAllPacks"
>
<template v-if="isLoading || isInstalling" #icon>
<DotSpinner duration="1s" :size="size === 'sm' ? 12 : 16" />
</template>
</IconTextButton>
</template>
<script setup lang="ts">
import { inject, ref } from 'vue'
import { computed } from 'vue'
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { t } from '@/i18n'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { ButtonSize } from '@/types/buttonTypes'
import {
IsInstallingKey,
ManagerChannel,
ManagerDatabaseSource,
SelectedVersion
@@ -28,14 +31,20 @@ import type { components } from '@/types/comfyRegistryTypes'
type NodePack = components['schemas']['Node']
const { nodePacks, variant, label } = defineProps<{
const {
nodePacks,
isLoading = false,
isInstalling = false,
label = 'Install',
size = 'sm'
} = defineProps<{
nodePacks: NodePack[]
variant?: 'default' | 'black'
isLoading?: boolean
isInstalling?: boolean
label?: string
size?: ButtonSize
}>()
const isInstalling = inject(IsInstallingKey, ref(false))
const managerStore = useComfyManagerStore()
const createPayload = (installItem: NodePack) => {
@@ -68,4 +77,11 @@ const installAllPacks = async () => {
await Promise.all(uninstalledPacks.map(installPack))
managerStore.installPack.clear()
}
const computedLabel = computed(() =>
isInstalling
? t('g.installing')
: label ??
(nodePacks.length > 1 ? t('manager.installSelected') : t('g.install'))
)
</script>

View File

@@ -1,27 +1,30 @@
<template>
<PackActionButton
<IconTextButton
v-bind="$attrs"
type="transparent"
:label="
nodePacks.length > 1
? $t('manager.uninstallSelected')
: $t('manager.uninstall')
"
severity="danger"
:loading-message="$t('manager.uninstalling')"
@action="uninstallItems"
:border="true"
:size="size"
class="border-red-500"
@click="uninstallItems"
/>
</template>
<script setup lang="ts">
import PackActionButton from '@/components/dialog/content/manager/button/PackActionButton.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { ButtonSize } from '@/types/buttonTypes'
import type { ManagerPackInfo } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
type NodePack = components['schemas']['Node']
const { nodePacks } = defineProps<{
const { nodePacks, size } = defineProps<{
nodePacks: NodePack[]
size?: ButtonSize
}>()
const managerStore = useComfyManagerStore()

View File

@@ -16,9 +16,16 @@
<PackUninstallButton
v-if="isAllInstalled"
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
/>
<PackInstallButton
v-else
v-bind="$attrs"
size="md"
:is-installing="isInstalling"
:node-packs="nodePacks"
/>
<PackInstallButton v-else v-bind="$attrs" :node-packs="nodePacks" />
</slot>
</div>
</div>
@@ -31,7 +38,7 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
@@ -56,4 +63,10 @@ watch(
},
{ immediate: true }
)
// Check if any of the packs are currently being installed
const isInstalling = computed(() => {
if (!nodePacks?.length) return false
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
})
</script>

View File

@@ -10,7 +10,19 @@
{{ $t('manager.packsSelected') }}
</template>
<template #install-button>
<PackInstallButton :full-width="true" :node-packs="nodePacks" />
<PackUninstallButton
v-if="isAllInstalled"
v-bind="$attrs"
size="md"
:node-packs="nodePacks"
/>
<PackInstallButton
v-else
v-bind="$attrs"
size="md"
:is-installing="isInstalling"
:node-packs="nodePacks"
/>
</template>
</InfoPanelHeader>
<div class="mb-6">
@@ -31,13 +43,15 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { computed, onUnmounted } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/components/dialog/content/manager/button/PackUninstallButton.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components } from '@/types/comfyRegistryTypes'
@@ -46,6 +60,24 @@ const { nodePacks } = defineProps<{
}>()
const { getNodeDefs } = useComfyRegistryStore()
const managerStore = useComfyManagerStore()
const isAllInstalled = ref(false)
watch(
[() => nodePacks, () => managerStore.installedPacks],
() => {
isAllInstalled.value = nodePacks.every((nodePack) =>
managerStore.isPackInstalled(nodePack.id)
)
},
{ immediate: true }
)
// Check if any of the packs are currently being installed
const isInstalling = computed(() => {
if (!nodePacks?.length) return false
return nodePacks.some((pack) => managerStore.isPackInstalling(pack.id))
})
const getPackNodes = async (pack: components['schemas']['Node']) => {
if (!pack.latest_version?.version) return []

View File

@@ -21,61 +21,48 @@
<PackBanner :node-pack="nodePack" />
</template>
<template #content>
<template v-if="isInstalling">
<div
class="self-stretch inline-flex flex-col justify-center items-center gap-2 h-full"
>
<ProgressSpinner />
<div
class="self-stretch text-center justify-start text-sm font-medium leading-none"
<div class="pt-4 px-4 pb-3 w-full h-full">
<div class="flex flex-col gap-y-1 w-full h-full">
<span
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
>
{{ $t('g.installing') }}...
</div>
</div>
</template>
<template v-else>
<div class="pt-4 px-4 pb-3 w-full h-full">
<div class="flex flex-col gap-y-1 w-full h-full">
<span
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
>
{{ nodePack.name }}
</span>
<p
v-if="nodePack.description"
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
>
{{ nodePack.description }}
</p>
<div class="flex flex-col gap-y-2">
<div class="flex-1 flex items-center gap-2">
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
{{ nodesCount }} {{ $t('g.nodes') }}
</div>
<PackVersionBadge
:node-pack="nodePack"
:is-selected="isSelected"
:fill="false"
/>
<div
v-if="formattedLatestVersionDate"
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
>
{{ formattedLatestVersionDate }}
</div>
{{ nodePack.name }}
</span>
<p
v-if="nodePack.description"
class="flex-1 text-muted text-xs font-medium break-words overflow-hidden min-h-12 line-clamp-3 my-0 leading-4 mb-1 overflow-hidden"
>
{{ nodePack.description }}
</p>
<div class="flex flex-col gap-y-2">
<div class="flex-1 flex items-center gap-2">
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
{{ nodesCount }} {{ $t('g.nodes') }}
</div>
<div class="flex">
<span
v-if="publisherName"
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
>
{{ publisherName }}
</span>
<PackVersionBadge
:node-pack="nodePack"
:is-selected="isSelected"
:fill="false"
:class="isInstalling ? 'pointer-events-none' : ''"
/>
<div
v-if="formattedLatestVersionDate"
class="px-2 py-1 flex justify-center items-center gap-1 text-xs text-muted font-medium"
>
{{ formattedLatestVersionDate }}
</div>
</div>
<div class="flex">
<span
v-if="publisherName"
class="text-xs text-muted font-medium leading-3 max-w-40 truncate"
>
{{ publisherName }}
</span>
</div>
</div>
</div>
</template>
</div>
</template>
<template #footer>
<PackCardFooter :node-pack="nodePack" />
@@ -85,7 +72,6 @@
<script setup lang="ts">
import Card from 'primevue/card'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, provide } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -6,18 +6,23 @@
<i class="pi pi-download text-muted"></i>
<span>{{ formattedDownloads }}</span>
</div>
<PackInstallButton v-if="!isInstalled" :node-packs="[nodePack]" />
<PackInstallButton
v-if="!isInstalled"
:node-packs="[nodePack]"
:is-installing="isInstalling"
/>
<PackEnableToggle v-else :node-pack="nodePack" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { IsInstallingKey } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
const { nodePack } = defineProps<{
@@ -26,6 +31,7 @@ const { nodePack } = defineProps<{
const { isPackInstalled } = useComfyManagerStore()
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
const isInstalling = inject(IsInstallingKey)
const { n } = useI18n()

View File

@@ -28,8 +28,8 @@
</div>
<PackInstallButton
v-if="isMissingTab && missingNodePacks.length > 0"
variant="black"
:disabled="isLoading || !!error"
:is-installing="isInstalling"
:node-packs="missingNodePacks"
:label="$t('manager.installAllMissingNodes')"
/>
@@ -67,6 +67,7 @@ import { useI18n } from 'vue-i18n'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import {
type SearchOption,
SortableAlgoliaField
@@ -96,6 +97,16 @@ const { t } = useI18n()
// Get missing node packs from workflow with loading and error states
const { missingNodePacks, isLoading, error } = useMissingNodes()
const comfyManagerStore = useComfyManagerStore()
// Check if any of the missing packs are currently being installed
const isInstalling = computed(() => {
if (!missingNodePacks.value?.length) return false
return missingNodePacks.value.some((pack) =>
comfyManagerStore.isPackInstalling(pack.id)
)
})
const hasResults = computed(
() => searchQuery.value?.trim() && searchResults?.length
)

View File

@@ -189,6 +189,7 @@
"noResultsFound": "No results found matching your search.",
"tryDifferentSearch": "Please try a different search query.",
"tryAgainLater": "Please try again later.",
"gettingInfo": "Getting info...",
"nodePack": "Node Pack",
"searchPlaceholder": "Search",
"version": "Version",
@@ -571,6 +572,8 @@
},
"templateWorkflows": {
"title": "Get Started with a Template",
"loadingMore": "Loading more templates...",
"searchPlaceholder": "Search templates...",
"category": {
"ComfyUI Examples": "ComfyUI Examples",
"Custom Nodes": "Custom Nodes",

View File

@@ -1,12 +1,18 @@
import type { HTMLAttributes } from 'vue'
export type ButtonSize = 'fit-content' | 'sm' | 'md'
export type ButtonType = 'primary' | 'secondary' | 'transparent'
export type ButtonBorder = boolean
export interface BaseButtonProps {
size?: 'fit-content' | 'sm' | 'md'
type?: 'primary' | 'secondary' | 'transparent'
size?: ButtonSize
type?: ButtonType
border?: ButtonBorder
disabled?: boolean
class?: HTMLAttributes['class']
}
export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
const sizeClasses = {
'fit-content': '',
sm: 'px-2 py-1.5 text-xs',
@@ -15,22 +21,39 @@ export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
return sizeClasses[size]
}
export const getButtonTypeClasses = (
type: BaseButtonProps['type'] = 'primary'
) => {
const typeClasses = {
export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
const baseByType = {
primary:
'bg-neutral-900 border-none text-white dark-theme:bg-white dark-theme:text-neutral-900',
secondary:
'bg-white border-none text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
transparent:
'bg-transparent border-none text-neutral-600 dark-theme:text-neutral-400'
} as const
return baseByType[type]
}
export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
const baseByType = {
primary:
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
secondary:
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
transparent: 'bg-transparent text-neutral-600 dark-theme:text-neutral-400'
}
return typeClasses[type]
} as const
const borderByType = {
primary: 'border border-solid border-white dark-theme:border-neutral-900',
secondary: 'border border-solid border-neutral-950 dark-theme:border-white',
transparent:
'border border-solid border-neutral-950 dark-theme:border-white'
} as const
return `${baseByType[type]} ${borderByType[type]}`
}
export const getIconButtonSizeClasses = (
size: BaseButtonProps['size'] = 'md'
) => {
export const getIconButtonSizeClasses = (size: ButtonSize = 'md') => {
const sizeClasses = {
'fit-content': 'w-auto h-auto',
sm: 'w-6 h-6 text-xs !rounded-md',
@@ -40,5 +63,9 @@ export const getIconButtonSizeClasses = (
}
export const getBaseButtonClasses = () => {
return 'flex items-center justify-center flex-shrink-0 outline-none border-none rounded-lg cursor-pointer transition-all duration-200'
return [
'flex items-center justify-center flex-shrink-0',
'outline-none rounded-lg cursor-pointer transition-all duration-200',
'disabled:opacity-50 disabled:pointer-events-none'
].join(' ')
}