@feugene/granularity 0.33.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{config-uzUuosv7.js → config-CT4lP6S4.js} +6 -5
- package/dist/chunks/config-CT4lP6S4.js.map +1 -0
- package/dist/chunks/{config-vYMJ3Cr1.js → config-CYthTVK2.js} +2 -2
- package/dist/chunks/config-CYthTVK2.js.map +1 -0
- package/dist/chunks/{config-CPSxM7XE.js → config-CsCUuukB.js} +1 -0
- package/dist/chunks/config-CsCUuukB.js.map +1 -0
- package/dist/chunks/{config-DnF472Ox.js → config-DOYz_SVK.js} +2 -2
- package/dist/chunks/{config-DnF472Ox.js.map → config-DOYz_SVK.js.map} +1 -1
- package/dist/chunks/{config-T3-DMXYm.js → config-DUNH02Fg.js} +2 -2
- package/dist/chunks/config-DUNH02Fg.js.map +1 -0
- package/dist/chunks/{config-BTwYMQaz2.js → config-DVCNvhIG2.js} +4 -3
- package/dist/chunks/config-DVCNvhIG2.js.map +1 -0
- package/dist/chunks/{generated-CSyPsWtM.js → generated-Bk-f6NsN.js} +21 -0
- package/dist/chunks/{generated-CSyPsWtM.js.map → generated-Bk-f6NsN.js.map} +1 -1
- package/dist/chunks/grDropdownStyles-DLToHWMX.js +5 -0
- package/dist/chunks/grDropdownStyles-DLToHWMX.js.map +1 -0
- package/dist/chunks/grPopoverStyles-CYAeO3Ov.js +110 -0
- package/dist/chunks/grPopoverStyles-CYAeO3Ov.js.map +1 -0
- package/dist/chunks/{grSelectStyles-Cdtd10eY.js → grSelectStyles-D9EQ-pgK.js} +2 -1
- package/dist/chunks/grSelectStyles-D9EQ-pgK.js.map +1 -0
- package/dist/chunks/{granular-provider-CrQua8PT.js → granular-provider-BAy4APOX.js} +6 -6
- package/dist/chunks/{granular-provider-CrQua8PT.js.map → granular-provider-BAy4APOX.js.map} +1 -1
- package/dist/chunks/{overlayTransition-0Fj5tQbq.js → overlayPanelSurface-BKeMhRx4.js} +24 -1
- package/dist/chunks/overlayPanelSurface-BKeMhRx4.js.map +1 -0
- package/dist/chunks/{focusables-BkzK42SQ.js → useFocusTrap-D9bIPNF6.js} +123 -1
- package/dist/chunks/useFocusTrap-D9bIPNF6.js.map +1 -0
- package/dist/chunks/{useMenuItemsFocus-B6TldmGf.js → useMenuItemsFocus-B7Bl_aFA.js} +1 -1
- package/dist/chunks/{useMenuItemsFocus-B6TldmGf.js.map → useMenuItemsFocus-B7Bl_aFA.js.map} +1 -1
- package/dist/chunks/{useModalOverlay-D__CqLGn.js → useModalOverlay-BnAGZpcI.js} +1 -1
- package/dist/chunks/{useModalOverlay-D__CqLGn.js.map → useModalOverlay-BnAGZpcI.js.map} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-CoQjEF8h.js → GrAutocomplete-BQsSE7sj.js} +2 -2
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-CoQjEF8h.js.map → GrAutocomplete-BQsSE7sj.js.map} +1 -1
- package/dist/components/GrAutocomplete/index.js +2 -2
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-p2XH_8cE.js → GrColorPicker-CIgdPXHn.js} +2 -1
- package/dist/components/GrColorPicker/chunks/GrColorPicker-CIgdPXHn.js.map +1 -0
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BXbh3q7O.js → GrCommandPalette-BZHUKlux.js} +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BXbh3q7O.js.map → GrCommandPalette-BZHUKlux.js.map} +1 -1
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-CUP43q__.js → GrConfirmDialog-jRDs_E6C.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-CUP43q__.js.map → GrConfirmDialog-jRDs_E6C.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-ClkVjdcP.js → GrContextMenu-DlFWm0eW.js} +17 -3
- package/dist/components/GrContextMenu/chunks/GrContextMenu-DlFWm0eW.js.map +1 -0
- package/dist/components/GrContextMenu/index.js +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-BiMJ9_T4.js → GrDialog-C01Hhk6G.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-BiMJ9_T4.js.map → GrDialog-C01Hhk6G.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-BxNK6crD.js → GrDialogService-CmsJNUmM.js} +7 -2
- package/dist/components/GrDialogService/chunks/{GrDialogService-BxNK6crD.js.map → GrDialogService-CmsJNUmM.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-Ce2l3zaG.js → GrDrawer-xfoy-MCj.js} +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-Ce2l3zaG.js.map → GrDrawer-xfoy-MCj.js.map} +1 -1
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-7r772-Cq.js +180 -0
- package/dist/components/GrDropdown/chunks/GrDropdown-7r772-Cq.js.map +1 -0
- package/dist/components/GrDropdown/index.js +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-GOzuAXsi.js → GrDropdownMenu-DNurbsWc.js} +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-GOzuAXsi.js.map → GrDropdownMenu-DNurbsWc.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-NsHkRRIa.js → GrFileUpload-LL8RZvC0.js} +10 -0
- package/dist/components/GrFileUpload/chunks/GrFileUpload-LL8RZvC0.js.map +1 -0
- package/dist/components/GrFileUpload/index.js +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CG4vUBg_.js → GrImageViewer--BU8yrfx.js} +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CG4vUBg_.js.map → GrImageViewer--BU8yrfx.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-D0mf1SAx.js → GrModal-_JrDhon-.js} +1 -1
- package/dist/components/GrModal/chunks/{GrModal-D0mf1SAx.js.map → GrModal-_JrDhon-.js.map} +1 -1
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-FiiXzVyv.js → GrPagination-gBoWVHk6.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-FiiXzVyv.js.map → GrPagination-gBoWVHk6.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-faJanmGs.js → GrPopover-DEFxDffj.js} +87 -13
- package/dist/components/GrPopover/chunks/GrPopover-DEFxDffj.js.map +1 -0
- package/dist/components/GrPopover/index.js +2 -2
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-C6QGC7QJ.js → GrPromptDialog-CailXSqW.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-C6QGC7QJ.js.map → GrPromptDialog-CailXSqW.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-Dt8iqruc.js → GrSelect-CZqV1Eg4.js} +2 -2
- package/dist/components/GrSelect/chunks/{GrSelect-Dt8iqruc.js.map → GrSelect-CZqV1Eg4.js.map} +1 -1
- package/dist/components/GrSelect/index.js +2 -2
- package/dist/components/GrTabs/chunks/{GrTabs-ugDnkk-2.js → GrTabs-UsO4j8zv.js} +5 -0
- package/dist/components/GrTabs/chunks/GrTabs-UsO4j8zv.js.map +1 -0
- package/dist/components/GrTabs/index.js +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-BHwgGJcT.js → GrTreeSelect-DOXcpWq2.js} +2 -2
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-BHwgGJcT.js.map → GrTreeSelect-DOXcpWq2.js.map} +1 -1
- package/dist/components/GrTreeSelect/index.js +2 -2
- package/dist/composables/useFloating.js +31 -11
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/composables/useFocusTrap.js +1 -123
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +26 -26
- package/dist/theme.js +1 -1
- package/dist/tokens.js +1 -1
- package/dist/types/components/GrAutocomplete/grAutocompleteStyles.d.ts +1 -1
- package/dist/types/components/GrDropdown/GrDropdown.vue.d.ts +5 -5
- package/dist/types/components/GrDropdown/grDropdownStyles.d.ts +8 -2
- package/dist/types/components/GrDropdownMenu/GrDropdownMenu.vue.d.ts +2 -2
- package/dist/types/components/GrPopover/GrPopover.vue.d.ts +54 -4
- package/dist/types/components/GrPopover/grPopoverStyles.d.ts +38 -11
- package/dist/types/components/GrSelect/grSelectStyles.d.ts +1 -1
- package/dist/types/components/GrTreeSelect/grTreeSelectStyles.d.ts +1 -1
- package/dist/types/components/shared/overlayOrigin.d.ts +22 -0
- package/dist/types/components/shared/overlayPanelSurface.d.ts +21 -0
- package/dist/types/composables/useFloating.d.ts +15 -0
- package/dist/web-types.json +40 -4
- package/package.json +1 -1
- package/dist/chunks/config-BTwYMQaz2.js.map +0 -1
- package/dist/chunks/config-CPSxM7XE.js.map +0 -1
- package/dist/chunks/config-T3-DMXYm.js.map +0 -1
- package/dist/chunks/config-uzUuosv7.js.map +0 -1
- package/dist/chunks/config-vYMJ3Cr1.js.map +0 -1
- package/dist/chunks/focusables-BkzK42SQ.js.map +0 -1
- package/dist/chunks/grDropdownStyles-V7TRiCXn.js +0 -24
- package/dist/chunks/grDropdownStyles-V7TRiCXn.js.map +0 -1
- package/dist/chunks/grPopoverStyles-Djh5RNhn.js +0 -59
- package/dist/chunks/grPopoverStyles-Djh5RNhn.js.map +0 -1
- package/dist/chunks/grSelectStyles-Cdtd10eY.js.map +0 -1
- package/dist/chunks/overlayTransition-0Fj5tQbq.js.map +0 -1
- package/dist/components/GrColorPicker/chunks/GrColorPicker-p2XH_8cE.js.map +0 -1
- package/dist/components/GrContextMenu/chunks/GrContextMenu-ClkVjdcP.js.map +0 -1
- package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js +0 -239
- package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js.map +0 -1
- package/dist/components/GrFileUpload/chunks/GrFileUpload-NsHkRRIa.js.map +0 -1
- package/dist/components/GrPopover/chunks/GrPopover-faJanmGs.js.map +0 -1
- package/dist/components/GrTabs/chunks/GrTabs-ugDnkk-2.js.map +0 -1
- package/dist/composables/useFocusTrap.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrModal-D0mf1SAx.js","names":["$slots"],"sources":["../../../../src/components/GrModal/context.ts","../../../../src/components/GrModal/GrModal.vue","../../../../src/components/GrModal/GrModal.vue"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport { inject, onScopeDispose, provide, watch } from 'vue'\n\nexport interface GrModalContext {\n /** Id, который обязан оказаться на элементе заголовка окна. */\n titleId: string\n /** Id, который обязан оказаться на элементе описания окна. */\n descriptionId: string\n /** «Заголовок отрисован» — окно ставит `aria-labelledby`. Возвращает отказ от заявки. */\n claimTitle: () => () => void\n /** То же для описания и `aria-describedby`. */\n claimDescription: () => () => void\n}\n\nexport const GR_MODAL_CONTEXT_KEY: InjectionKey<GrModalContext> = Symbol('GrModalContext')\n\nexport function provideGrModalContext(context: GrModalContext): void {\n provide(GR_MODAL_CONTEXT_KEY, context)\n}\n\n/**\n * Контекст окна для его частей.\n *\n * Именно контекст, а не проп: заголовок рисует `GrDialogHeader` — внук\n * `GrModal` через слот, — а `aria-labelledby` стоит на корне окна.\n *\n * **Id принадлежит окну, а не заголовку.** Заголовок только сообщает, что\n * отрисовался. Обратный порядок (id рождается у потомка и уезжает наверх)\n * означает мутацию состояния окна во время его собственного патча: Vue уходит\n * в рекурсивный ре-рендер и монтирует заголовок заново, пока не упрётся в\n * лимит. Стабильный id этого класса ошибок не допускает по построению.\n *\n * `null` вне окна — законное состояние: `GrDialogHeader` используют и отдельно.\n */\nexport function useGrModalContext(): GrModalContext | null {\n return inject(GR_MODAL_CONTEXT_KEY, null)\n}\n\n/**\n * Объявляет элемент заголовком окна, пока `active` истинно.\n *\n * Возвращает id для элемента — или `undefined` вне окна: тогда заголовок просто\n * рисуется без связи.\n */\nexport function useGrModalTitle(active: () => boolean): { titleId: string | undefined } {\n const context = useGrModalContext()\n if (!context)\n return { titleId: undefined }\n\n let release: (() => void) | undefined\n\n watch(\n active,\n (isActive) => {\n release?.()\n release = isActive ? context.claimTitle() : undefined\n },\n // `post`: заявка меняет состояние окна, а заголовок — его потомок, и в\n // `pre`-фазе мутация пришлась бы на патч самого окна.\n { immediate: true, flush: 'post' },\n )\n\n onScopeDispose(() => release?.())\n\n return { titleId: context.titleId }\n}\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrModal-_JrDhon-.js","names":["$slots"],"sources":["../../../../src/components/GrModal/context.ts","../../../../src/components/GrModal/GrModal.vue","../../../../src/components/GrModal/GrModal.vue"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport { inject, onScopeDispose, provide, watch } from 'vue'\n\nexport interface GrModalContext {\n /** Id, который обязан оказаться на элементе заголовка окна. */\n titleId: string\n /** Id, который обязан оказаться на элементе описания окна. */\n descriptionId: string\n /** «Заголовок отрисован» — окно ставит `aria-labelledby`. Возвращает отказ от заявки. */\n claimTitle: () => () => void\n /** То же для описания и `aria-describedby`. */\n claimDescription: () => () => void\n}\n\nexport const GR_MODAL_CONTEXT_KEY: InjectionKey<GrModalContext> = Symbol('GrModalContext')\n\nexport function provideGrModalContext(context: GrModalContext): void {\n provide(GR_MODAL_CONTEXT_KEY, context)\n}\n\n/**\n * Контекст окна для его частей.\n *\n * Именно контекст, а не проп: заголовок рисует `GrDialogHeader` — внук\n * `GrModal` через слот, — а `aria-labelledby` стоит на корне окна.\n *\n * **Id принадлежит окну, а не заголовку.** Заголовок только сообщает, что\n * отрисовался. Обратный порядок (id рождается у потомка и уезжает наверх)\n * означает мутацию состояния окна во время его собственного патча: Vue уходит\n * в рекурсивный ре-рендер и монтирует заголовок заново, пока не упрётся в\n * лимит. Стабильный id этого класса ошибок не допускает по построению.\n *\n * `null` вне окна — законное состояние: `GrDialogHeader` используют и отдельно.\n */\nexport function useGrModalContext(): GrModalContext | null {\n return inject(GR_MODAL_CONTEXT_KEY, null)\n}\n\n/**\n * Объявляет элемент заголовком окна, пока `active` истинно.\n *\n * Возвращает id для элемента — или `undefined` вне окна: тогда заголовок просто\n * рисуется без связи.\n */\nexport function useGrModalTitle(active: () => boolean): { titleId: string | undefined } {\n const context = useGrModalContext()\n if (!context)\n return { titleId: undefined }\n\n let release: (() => void) | undefined\n\n watch(\n active,\n (isActive) => {\n release?.()\n release = isActive ? context.claimTitle() : undefined\n },\n // `post`: заявка меняет состояние окна, а заголовок — его потомок, и в\n // `pre`-фазе мутация пришлась бы на патч самого окна.\n { immediate: true, flush: 'post' },\n )\n\n onScopeDispose(() => release?.())\n\n return { titleId: context.titleId }\n}\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrModal_default } from "./chunks/GrModal-
|
|
1
|
+
import { t as GrModal_default } from "./chunks/GrModal-_JrDhon-.js";
|
|
2
2
|
import { n as grModalSafelist, t as grModalConfig } from "../../chunks/config-mlheIMtV.js";
|
|
3
3
|
export { GrModal_default as GrModal, GrModal_default as default, grModalConfig, grModalSafelist };
|
package/dist/components/GrPagination/chunks/{GrPagination-FiiXzVyv.js → GrPagination-gBoWVHk6.js}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as useGrComponentSize } from "../../../chunks/context-CTbuwcYs.js";
|
|
2
2
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
3
3
|
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-7GP0zObj.js";
|
|
4
|
-
import { t as GrSelect_default } from "../../GrSelect/chunks/GrSelect-
|
|
4
|
+
import { t as GrSelect_default } from "../../GrSelect/chunks/GrSelect-CZqV1Eg4.js";
|
|
5
5
|
import { a as labelSizes, c as pageSizeSelectWidths, i as jumperSizes, l as pageSizes, o as navButtonSizes, r as ellipsisSizes, s as pageListGaps, u as rowGaps } from "../../../chunks/config-CjKpUcmy.js";
|
|
6
6
|
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, ref, renderList, renderSlot, toDisplayString, unref, vModelText, watch, withCtx, withDirectives, withKeys } from "vue";
|
|
7
7
|
//#region src/components/GrPagination/GrPagination.vue?vue&type=script&setup=true&lang.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPagination-FiiXzVyv.js","names":[],"sources":["../../../../src/components/GrPagination/GrPagination.vue","../../../../src/components/GrPagination/GrPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrPagination-gBoWVHk6.js","names":[],"sources":["../../../../src/components/GrPagination/GrPagination.vue","../../../../src/components/GrPagination/GrPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as grPaginationSafelist, t as grPaginationConfig } from "../../chunks/config-CjKpUcmy.js";
|
|
2
|
-
import { t as GrPagination_default } from "./chunks/GrPagination-
|
|
2
|
+
import { t as GrPagination_default } from "./chunks/GrPagination-gBoWVHk6.js";
|
|
3
3
|
export { GrPagination_default as GrPagination, GrPagination_default as default, grPaginationConfig, grPaginationSafelist };
|
|
@@ -2,10 +2,10 @@ import { a as useGrComponentSize } from "../../../chunks/context-CTbuwcYs.js";
|
|
|
2
2
|
import { vClickOutside } from "../../../directives/clickOutside.js";
|
|
3
3
|
import { createFloatingAnchor, useFloating } from "../../../composables/useFloating.js";
|
|
4
4
|
import { t as useControlledOpen } from "../../../chunks/useControlledOpen-DV3SqLTP.js";
|
|
5
|
-
import {
|
|
6
|
-
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-
|
|
7
|
-
import { t as grPopoverPanelClass } from "../../../chunks/grPopoverStyles-
|
|
8
|
-
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, mergeProps, nextTick, openBlock, ref, renderSlot, unref, useId, useSlots, vShow, watch, withCtx, withDirectives } from "vue";
|
|
5
|
+
import { n as panelPopTransition } from "../../../chunks/overlayPanelSurface-BKeMhRx4.js";
|
|
6
|
+
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-BnAGZpcI.js";
|
|
7
|
+
import { t as grPopoverPanelClass } from "../../../chunks/grPopoverStyles-CYAeO3Ov.js";
|
|
8
|
+
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, mergeProps, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, renderSlot, unref, useId, useSlots, vShow, watch, withCtx, withDirectives } from "vue";
|
|
9
9
|
//#region src/components/GrPopover/GrPopover.vue?vue&type=script&setup=true&lang.ts
|
|
10
10
|
var _hoisted_1 = { "data-gr-popover": "" };
|
|
11
11
|
var _hoisted_2 = [
|
|
@@ -36,6 +36,8 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
36
36
|
ariaLabel: { default: void 0 },
|
|
37
37
|
labelledBy: { default: void 0 },
|
|
38
38
|
trigger: { default: "click" },
|
|
39
|
+
openDelay: { default: 120 },
|
|
40
|
+
closeDelay: { default: 160 },
|
|
39
41
|
closeOnEsc: {
|
|
40
42
|
type: Boolean,
|
|
41
43
|
default: true
|
|
@@ -59,7 +61,15 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
59
61
|
default: false
|
|
60
62
|
},
|
|
61
63
|
anchor: { default: void 0 },
|
|
62
|
-
padding: { default: "default" }
|
|
64
|
+
padding: { default: "default" },
|
|
65
|
+
matchWidth: {
|
|
66
|
+
type: [Boolean, String],
|
|
67
|
+
default: false
|
|
68
|
+
},
|
|
69
|
+
block: {
|
|
70
|
+
type: Boolean,
|
|
71
|
+
default: false
|
|
72
|
+
}
|
|
63
73
|
},
|
|
64
74
|
emits: ["update:open"],
|
|
65
75
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
@@ -94,11 +104,14 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
94
104
|
const panelEl = ref(null);
|
|
95
105
|
const panelId = useId();
|
|
96
106
|
const clickOutsideExclude = [() => panelEl.value];
|
|
107
|
+
let hoverTimer;
|
|
97
108
|
function open() {
|
|
98
109
|
if (props.disabled) return;
|
|
110
|
+
clearTimeout(hoverTimer);
|
|
99
111
|
setOpen(true);
|
|
100
112
|
}
|
|
101
113
|
function close() {
|
|
114
|
+
clearTimeout(hoverTimer);
|
|
102
115
|
setOpen(false);
|
|
103
116
|
}
|
|
104
117
|
function toggle() {
|
|
@@ -115,7 +128,8 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
115
128
|
const { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(() => props.anchor ? anchorEl : rootEl.value, panelEl, isOpen, {
|
|
116
129
|
placement: () => props.placement,
|
|
117
130
|
offsetPx: props.offsetPx,
|
|
118
|
-
zIndexVar: "--gr-z-dropdown"
|
|
131
|
+
zIndexVar: "--gr-z-dropdown",
|
|
132
|
+
matchWidth: () => props.matchWidth
|
|
119
133
|
});
|
|
120
134
|
watch(() => props.placement, () => {
|
|
121
135
|
if (isOpen.value) updateFloatingPosition();
|
|
@@ -157,18 +171,74 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
157
171
|
];
|
|
158
172
|
/**
|
|
159
173
|
* Пропсы для реального фокусируемого триггера: `<button v-bind="triggerProps">`.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
174
|
+
*
|
|
175
|
+
* Здесь и ARIA, и **клик**. ARIA — потому что `aria-expanded` обязан жить на том
|
|
176
|
+
* узле, который получает фокус, а не на `div` вокруг него. Клик — потому что
|
|
177
|
+
* обёртка ловит и клики по вложенным элементам: кнопка «Ещё» рядом с триггером,
|
|
178
|
+
* ссылка в карточке-триггере, крестик на чипе открывали бы панель мимо намерения
|
|
179
|
+
* пользователя. То же решение и по той же причине — в `GrDropdown`.
|
|
162
180
|
*/
|
|
163
181
|
const triggerProps = computed(() => ({
|
|
164
182
|
"aria-haspopup": HASPOPUP_ROLES.includes(props.role) ? props.role : void 0,
|
|
165
183
|
"aria-expanded": isOpen.value,
|
|
166
184
|
"aria-controls": isOpen.value ? panelId : void 0,
|
|
167
|
-
"disabled": props.disabled || void 0
|
|
185
|
+
"disabled": props.disabled || void 0,
|
|
186
|
+
"onClick": onTriggerClick
|
|
168
187
|
}));
|
|
169
188
|
function onTriggerClick() {
|
|
170
|
-
if (props.trigger === "click") toggle();
|
|
189
|
+
if (props.trigger === "click" || props.trigger === "hover") toggle();
|
|
190
|
+
}
|
|
191
|
+
/** Привязан ли `triggerProps`: `aria-expanded` он ставит всегда, при любой роли. */
|
|
192
|
+
function hasBoundTrigger() {
|
|
193
|
+
return rootEl.value?.querySelector("[aria-expanded]") != null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Клик по обёртке — совместимость, а не механизм.
|
|
197
|
+
*
|
|
198
|
+
* Клик триггера живёт в `triggerProps`. Но слот, оставленный без
|
|
199
|
+
* `v-bind="triggerProps"`, до этой правки открывался кликом по обёртке, и молча
|
|
200
|
+
* отнимать это у тех, кто на это опирался, нельзя. Поэтому обёртка срабатывает
|
|
201
|
+
* ровно тогда, когда привязанного триггера внутри нет.
|
|
202
|
+
*
|
|
203
|
+
* Проверка на месте клика, а не при монтировании: содержимое слота реактивно, и
|
|
204
|
+
* триггер может появиться позже. `querySelector` по одному узлу на клик дешевле
|
|
205
|
+
* подписки на мутации.
|
|
206
|
+
*/
|
|
207
|
+
function onWrapperClick() {
|
|
208
|
+
if (hasBoundTrigger()) return;
|
|
209
|
+
onTriggerClick();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Открытие по наведению.
|
|
213
|
+
*
|
|
214
|
+
* Таймер один на обе стороны и сбрасывается при каждом событии: курсор,
|
|
215
|
+
* прошедший триггер насквозь, не должен оставить за собой отложенное открытие.
|
|
216
|
+
* Нулевая задержка выполняется сразу, а не через `setTimeout(0)`, — иначе
|
|
217
|
+
* открытие уезжало бы на следующий тик и ломало тесты, которые ждут синхронности.
|
|
218
|
+
*/
|
|
219
|
+
function scheduleHover(next, delayMs) {
|
|
220
|
+
clearTimeout(hoverTimer);
|
|
221
|
+
if (delayMs <= 0) {
|
|
222
|
+
setOpen(next);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
hoverTimer = setTimeout(setOpen, delayMs, next);
|
|
226
|
+
}
|
|
227
|
+
function onHoverEnter() {
|
|
228
|
+
if (props.disabled || props.trigger !== "hover") return;
|
|
229
|
+
scheduleHover(true, props.openDelay);
|
|
230
|
+
}
|
|
231
|
+
function onHoverLeave() {
|
|
232
|
+
if (props.trigger !== "hover") return;
|
|
233
|
+
scheduleHover(false, props.closeDelay);
|
|
171
234
|
}
|
|
235
|
+
onMounted(() => {
|
|
236
|
+
if (!(typeof process !== "undefined" && process.env.NODE_ENV !== "production") || !hasTrigger.value || hasBoundTrigger()) return;
|
|
237
|
+
console.warn("[GrPopover] триггер не привязан: добавьте `v-bind=\"triggerProps\"` на элемент внутри слота #trigger — <template #trigger=\"{ triggerProps }\"><GrButton v-bind=\"triggerProps\">…</GrButton></template>. Клик по обёртке пока работает, но у триггера нет ни клавиатуры, ни aria-haspopup/aria-expanded, а клик по вложенным элементам открывает панель.");
|
|
238
|
+
});
|
|
239
|
+
onBeforeUnmount(() => {
|
|
240
|
+
clearTimeout(hoverTimer);
|
|
241
|
+
});
|
|
172
242
|
function onContentClick() {
|
|
173
243
|
if (props.closeOnContentClick) close();
|
|
174
244
|
}
|
|
@@ -184,14 +254,16 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
184
254
|
ref_key: "rootEl",
|
|
185
255
|
ref: rootEl,
|
|
186
256
|
"data-gr-popover-trigger": "",
|
|
187
|
-
class: "inline-block max-w-full",
|
|
188
|
-
onClick:
|
|
257
|
+
class: normalizeClass(__props.block ? "block w-full" : "inline-block max-w-full"),
|
|
258
|
+
onClick: onWrapperClick,
|
|
259
|
+
onMouseenter: onHoverEnter,
|
|
260
|
+
onMouseleave: onHoverLeave
|
|
189
261
|
}, [renderSlot(_ctx.$slots, "trigger", {
|
|
190
262
|
open: unref(isOpen),
|
|
191
263
|
toggle,
|
|
192
264
|
close,
|
|
193
265
|
triggerProps: triggerProps.value
|
|
194
|
-
})])), [[unref(vClickOutside), {
|
|
266
|
+
})], 34)), [[unref(vClickOutside), {
|
|
195
267
|
handler: close,
|
|
196
268
|
enabled: unref(isOpen) && __props.closeOnClickOutside,
|
|
197
269
|
exclude: clickOutsideExclude
|
|
@@ -221,6 +293,8 @@ var GrPopover_default = /* @__PURE__ */ defineComponent({
|
|
|
221
293
|
tabindex: "-1",
|
|
222
294
|
class: panelClasses.value,
|
|
223
295
|
style: unref(floatingStyle),
|
|
296
|
+
onMouseenter: onHoverEnter,
|
|
297
|
+
onMouseleave: onHoverLeave,
|
|
224
298
|
onClick: onContentClick
|
|
225
299
|
}), [renderSlot(_ctx.$slots, "content", { close })], 16, _hoisted_2)), [[vShow, unref(isOpen)], [unref(vClickOutside), {
|
|
226
300
|
handler: close,
|