diff --git a/eslint.config.js b/eslint.config.js index 16e834e950..46effc413e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -99,6 +99,10 @@ export default vuepress( ], '@typescript-eslint/no-dynamic-delete': 'off', '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-function': [ + 'error', + { allow: ['protected-constructors'] }, + ], '@typescript-eslint/no-floating-promises': [ 'error', { diff --git a/tools/helper/package.json b/tools/helper/package.json index 8240e7f589..02a76f1d84 100644 --- a/tools/helper/package.json +++ b/tools/helper/package.json @@ -31,6 +31,7 @@ "./noopComponent": "./lib/client/noopComponent.js", "./noopModule": "./lib/client/noopModule.js", "./colors.css": "./lib/client/styles/colors.css", + "./message.css": "./lib/client/styles/message.css", "./normalize.css": "./lib/client/styles/normalize.css", "./sr-only.css": "./lib/client/styles/sr-only.css", "./transition/*.css": "./lib/client/styles/transition/*.css", diff --git a/tools/helper/src/client/styles/message.scss b/tools/helper/src/client/styles/message.scss new file mode 100644 index 0000000000..7e44a34c5f --- /dev/null +++ b/tools/helper/src/client/styles/message.scss @@ -0,0 +1,73 @@ +:root { + --message-offset: calc(var(--vp-header-offset, 3.6rem) + 1rem); + --message-timing-duration: 0.3s; + --message-timing-function: ease-in-out; + --message-gap: 0.5rem; +} + +@keyframes message-move-in { + 0% { + opacity: 0; + transform: translateY(-10px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes message-move-out { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(-100%); + } +} + +#message-container { + position: fixed; + inset: var(--message-offset) 0 auto; + z-index: 75; + + display: flex; + flex-flow: column; + gap: var(--message-gap); + align-items: center; + + text-align: center; +} + +.message-item { + display: inline-block; + + padding: 8px 10px; + border-radius: 3px; + + background: var(--vp-c-bg); + color: var(--vp-c-text); + box-shadow: 0 0 10px 0 var(--vp-c-shadow); + + font-size: 14px; + + &.move-in { + animation: message-move-in var(--message-timing-duration) + var(--message-timing-function); + } + + &.move-out { + animation: message-move-out var(--message-timing-duration) + var(--message-timing-function); + animation-fill-mode: forwards; + } + + svg { + position: relative; + bottom: -0.125em; + margin-inline-end: 5px; + } +} diff --git a/tools/helper/src/client/utils/index.ts b/tools/helper/src/client/utils/index.ts index 452cc47b3f..8babf00fb9 100644 --- a/tools/helper/src/client/utils/index.ts +++ b/tools/helper/src/client/utils/index.ts @@ -5,4 +5,5 @@ export * from './getHeaders.js' export * from './isFocusingTextControl.js' export * from './isKeyMatched.js' export * from './hasGlobalComponent.js' +export * from './message.js' export * from './wait.js' diff --git a/tools/helper/src/client/utils/message.ts b/tools/helper/src/client/utils/message.ts new file mode 100644 index 0000000000..5abbdef690 --- /dev/null +++ b/tools/helper/src/client/utils/message.ts @@ -0,0 +1,71 @@ +import { keys } from '../../shared/index.js' + +const containerId = 'message-container' + +export class Message { + private elements: Record + + public constructor() { + this.elements = {} + } + + public static get containerElement(): HTMLElement { + let containerElement = document.getElementById(containerId) + + if (containerElement) return containerElement + + containerElement = document.createElement('div') + containerElement.id = containerId + document.body.appendChild(containerElement) + + return containerElement + } + + public getElement(messageId: number): HTMLDivElement { + return this.elements[messageId] + } + + public pop(html: string, duration = 2000, clickToClose = true): number { + const messageId = Date.now() + const messageElement = document.createElement('div') + messageElement.className = 'message-item move-in' + messageElement.innerHTML = html + Message.containerElement.appendChild(messageElement) + this.elements[messageId] = messageElement + + if (clickToClose) + messageElement.addEventListener('click', () => { + this.close(messageId) + }) + + if (duration > 0) + setTimeout(() => { + this.close(messageId) + }, duration) + + return messageId + } + + public close(messageId?: number): void { + if (messageId) { + const messageElement = this.elements[messageId] + + messageElement.classList.remove('move-in') + messageElement.classList.add('move-out') + messageElement.addEventListener('animationend', () => { + messageElement.remove() + delete this.elements[messageId] + }) + } else { + keys(this.elements).forEach((id) => { + this.close(Number(id)) + }) + } + } + + public destroy(): void { + const containerElement = document.getElementById(containerId) + if (containerElement) document.body.removeChild(containerElement) + this.elements = {} + } +}