@feugene/granularity 0.25.0 → 0.26.0
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/{validation-DauXRIkL.js → validation-5HhpanPP.js} +1 -1
- package/dist/chunks/{validation-DauXRIkL.js.map → validation-5HhpanPP.js.map} +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-CHhzGRWy.js → GrDialogService-CHVR4sOk.js} +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-CHhzGRWy.js.map → GrDialogService-CHVR4sOk.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrForm/chunks/{GrForm-B_Qq_13k.js → GrForm-CAlJg-wb.js} +1 -1
- package/dist/components/GrForm/chunks/{GrForm-B_Qq_13k.js.map → GrForm-CAlJg-wb.js.map} +1 -1
- package/dist/components/GrForm/index.js +3 -3
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DjF-5oD9.js → GrPromptDialog-BGuMUzlv.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DjF-5oD9.js.map → GrPromptDialog-BGuMUzlv.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/index.js +5 -5
- package/dist/types/components/GrForm/index.d.ts +1 -1
- package/dist/web-types.json +1 -1
- package/package.json +1 -1
|
@@ -175,4 +175,4 @@ async function runFieldRules(value, rules, model, resolveMessage) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
//#endregion
|
|
178
|
-
export {
|
|
178
|
+
export { rulesRequired as a, toRuleArray as c, rulesForTrigger as i, getByPath as n, runFieldRules as o, isEmpty as r, setByPath as s, createGrFormMessageResolver as t };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation-DauXRIkL.js","names":[],"sources":["../../src/components/GrForm/fileRule.ts","../../src/components/GrForm/validation.ts"],"sourcesContent":["/**\n * Файловое правило `GrForm` — мост между декларативными `rules` и валидаторами\n * из `src/fileValidation`.\n *\n * Своих проверок здесь нет ни одной: правило собирает те же валидаторы, которые\n * `GrFormFile` и `v-dropzone` запускают на выборе и на drop. Поэтому ограничение,\n * переехавшее из пропов поля в правило формы, не меняет ни поведения, ни текста\n * ошибки — меняется только момент проверки и то, кто о ней сообщает.\n *\n * Модуль чистый (без Vue): его гоняют юнит-тесты без монтирования.\n */\n\nimport {\n acceptValidator,\n allowedExtensionsValidator,\n allowedMimeTypesValidator,\n maxCountValidator,\n maxFileSize,\n maxTotalSizeBytesValidator,\n runFileValidators,\n type FileValidationIssue,\n type FileValidator,\n} from '../../fileValidation'\n\nexport interface GrFormFileRule {\n /** W3C `accept`: `'image/*,.pdf'`. */\n accept?: string\n /** Белый список расширений (с точкой или без). */\n extensions?: string[]\n /** Белый список MIME-типов. */\n mimeTypes?: string[]\n /** Предел размера одного файла в мегабайтах. */\n maxSizeMb?: number\n /** Предел размера одного файла в байтах. Задан вместе с `maxSizeMb` — берётся меньший. */\n maxSizeBytes?: number\n /** Максимум файлов в наборе. */\n maxCount?: number\n /** Суммарный размер набора в мегабайтах. */\n maxTotalSizeMb?: number\n /** Свои валидаторы — тот же тип, что у `GrFormFile` и `v-dropzone`. Идут после встроенных. */\n validators?: FileValidator[]\n}\n\nconst BYTES_IN_MB = 1024 * 1024\n\nfunction mbToBytes(mb: number | undefined): number | undefined {\n return typeof mb === 'number' && Number.isFinite(mb) ? mb * BYTES_IN_MB : undefined\n}\n\nfunction isFile(value: unknown): value is File {\n return typeof File !== 'undefined' && value instanceof File\n}\n\n/**\n * Значение поля как набор файлов. Всё, что файлами не является, даёт пустой\n * набор: правило молчит, а пустоту разбирает `required` движка.\n */\nexport function fileRuleFiles(value: unknown): File[] {\n if (isFile(value)) return [value]\n if (Array.isArray(value)) return value.filter(isFile)\n return []\n}\n\n/** Порядок повторяет `GrFormFile`: встроенные проверки, затем валидаторы потребителя. */\nexport function fileRuleValidators(rule: GrFormFileRule): FileValidator[] {\n return [\n acceptValidator(rule.accept),\n ...(rule.extensions ? [allowedExtensionsValidator(rule.extensions)] : []),\n ...(rule.mimeTypes ? [allowedMimeTypesValidator(rule.mimeTypes)] : []),\n maxFileSize({ bytes: rule.maxSizeBytes, mb: rule.maxSizeMb }),\n maxCountValidator(rule.maxCount),\n maxTotalSizeBytesValidator(mbToBytes(rule.maxTotalSizeMb)),\n ...(rule.validators ?? []),\n ]\n}\n\n/**\n * Первая проблема набора или `undefined`. Первая, а не все: у поля формы одна\n * строка ошибки, и файлы тут не исключение — подробный разбор по каждому файлу\n * остаётся за `GrFormFile`.\n */\nexport async function runFileRule(\n value: unknown,\n rule: GrFormFileRule,\n): Promise<FileValidationIssue | undefined> {\n const files = fileRuleFiles(value)\n if (!files.length) return undefined\n\n const { issues } = await runFileValidators(files, fileRuleValidators(rule), {\n source: 'form',\n multiple: Array.isArray(value),\n accept: rule.accept,\n })\n\n return issues[0]\n}\n","/**\n * Движок валидации `GrForm` — чистый (без Vue/i18n), тестируется изолированно.\n *\n * Правило описывает набор проверок значения поля; `runFieldRules` прогоняет их по\n * порядку и возвращает первое сообщение об ошибке (или `undefined`, если валидно).\n * Сообщения резолвит потребитель (компонент) через `resolveMessage` — так i18n и\n * дефолтные тексты остаются в компоненте, а движок ничего о них не знает.\n */\n\nimport { resolveFileValidationMessage, type FileValidationIssue } from '../../fileValidation'\nimport { runFileRule, type GrFormFileRule } from './fileRule'\n\nexport type { GrFormFileRule } from './fileRule'\n\nexport type GrFormTrigger = 'blur' | 'change' | 'submit'\n\n/** Результат кастомного валидатора: `true`/void — ок; `false` — ошибка (дефолт-текст); строка — текст ошибки. */\nexport type GrFormValidatorResult = boolean | string | void\n\nexport interface GrFormRule {\n /** Значение обязательно (не пустое). */\n required?: boolean\n /** Готовый текст ошибки — перекрывает дефолтное сообщение любой проверки этого правила. */\n message?: string\n /** Минимум: длина для строки/массива, значение для числа. */\n min?: number\n /** Максимум: длина для строки/массива, значение для числа. */\n max?: number\n /** Точная длина/значение. */\n len?: number\n /** Регулярное выражение (проверяется по `String(value)`). */\n pattern?: RegExp\n /** Встроенный тип-валидатор. */\n type?: 'email' | 'url'\n /** Ограничения для значения `File | File[]`: тип, размер, количество. */\n file?: GrFormFileRule\n /** Кастомный (в т.ч. async) валидатор. Второй аргумент — весь `model`. */\n validator?: (value: unknown, model: Record<string, unknown>) => GrFormValidatorResult | Promise<GrFormValidatorResult>\n /** Когда правило срабатывает. По умолчанию — на любом триггере (blur/change/submit). */\n trigger?: GrFormTrigger | GrFormTrigger[]\n}\n\nexport type GrFormRules = Record<string, GrFormRule | GrFormRule[]>\n\n/** Вид сработавшей проверки — ключ для дефолтного i18n-сообщения. */\nexport type GrFormRuleFailure = 'required' | 'min' | 'max' | 'len' | 'pattern' | 'email' | 'url' | 'file' | 'invalid'\n\nexport type GrFormMessageResolver = (\n kind: GrFormRuleFailure,\n rule: GrFormRule,\n params: Record<string, unknown>,\n) => string\n\n/** Подпись `t()` из `useGranularityTranslations()`: ключ, фолбэк, параметры. */\nexport type GrFormMessageTranslate = (\n key: string,\n fallback: string,\n params?: Record<string, unknown>,\n) => string\n\nconst DEFAULT_MESSAGES: Record<GrFormRuleFailure, string> = {\n required: 'This field is required',\n min: 'Minimum is {min}',\n max: 'Maximum is {max}',\n len: 'Length must be {len}',\n pattern: 'Invalid format',\n email: 'Enter a valid email',\n url: 'Enter a valid URL',\n file: 'Invalid file',\n invalid: 'Invalid value',\n}\n\n/**\n * Дефолтный резолвер сообщений: `rule.message` сильнее всего, иначе ключ\n * `gr.form.<kind>` из локали, иначе английский фолбэк.\n *\n * Вынесен из `GrForm.vue` и экспортируется наружу не «на всякий случай»:\n * `runFieldRules` публичен, но без резолвера бесполезен — любой, кто хочет\n * прогнать те же правила вне формы, обязан был написать свой.\n */\nexport function createGrFormMessageResolver(t: GrFormMessageTranslate): GrFormMessageResolver {\n return (kind, rule, params) => {\n if (rule.message) return rule.message\n\n // Текст файловой ошибки приходит от самого валидатора и локализуется его же\n // ключом (`gr.fileValidation.*`): иначе одно и то же «файл не того типа»\n // звучало бы по-разному в поле и в форме. `gr.form.file` — фолбэк на случай\n // вида `file` без issue: сюда движок не приходит, но резолвер публичный.\n const issue = params.issue as FileValidationIssue | undefined\n if (kind === 'file' && issue) return resolveFileValidationMessage(issue, t)\n\n return t(`gr.form.${kind}`, DEFAULT_MESSAGES[kind], params)\n }\n}\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isUrl(value: string): boolean {\n return URL.canParse(value)\n}\n\n/** Пустое значение: `null`/`undefined`/пустая (или пробельная) строка/пустой массив. */\nexport function isEmpty(value: unknown): boolean {\n if (value == null) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n return false\n}\n\n/** Мера значения для min/max/len: число → само значение, строка/массив → длина. */\nfunction measure(value: unknown): number {\n if (typeof value === 'number') return value\n if (typeof value === 'string') return value.length\n if (Array.isArray(value)) return value.length\n return Number(value)\n}\n\n/** Достаёт значение из объекта по dot-path (`address.city`). */\nexport function getByPath(obj: Record<string, unknown>, path: string): unknown {\n if (!path.includes('.')) return obj[path]\n return path.split('.').reduce<unknown>((acc, key) => {\n if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[key]\n return undefined\n }, obj)\n}\n\n/** Записывает значение в объект по dot-path (создаёт промежуточные объекты). */\nexport function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split('.')\n const last = keys.pop() as string\n let target = obj\n for (const key of keys) {\n if (!target[key] || typeof target[key] !== 'object') target[key] = {}\n target = target[key] as Record<string, unknown>\n }\n target[last] = value\n}\n\n/** Нормализует правило(а) поля в массив. */\nexport function toRuleArray(rules: GrFormRule | GrFormRule[] | undefined): GrFormRule[] {\n if (!rules) return []\n return Array.isArray(rules) ? rules : [rules]\n}\n\nfunction ruleMatchesTrigger(rule: GrFormRule, trigger?: GrFormTrigger): boolean {\n if (!trigger) return true // полная валидация (validate()) — все правила\n if (!rule.trigger) return true // правило без триггера срабатывает на любом\n const triggers = Array.isArray(rule.trigger) ? rule.trigger : [rule.trigger]\n return triggers.includes(trigger)\n}\n\n/** Правила поля, применимые для данного триггера (undefined = все). */\nexport function rulesForTrigger(rules: GrFormRule[], trigger?: GrFormTrigger): GrFormRule[] {\n return rules.filter(rule => ruleMatchesTrigger(rule, trigger))\n}\n\n/** Есть ли среди правил `required` (для маркера обязательности в поле). */\nexport function rulesRequired(rules: GrFormRule[]): boolean {\n return rules.some(rule => rule.required === true)\n}\n\n/**\n * Прогоняет правила по порядку, возвращает первое сообщение об ошибке или `undefined`.\n * Пустое необязательное значение проходит все проверки (кроме `required`).\n */\nexport async function runFieldRules(\n value: unknown,\n rules: GrFormRule[],\n model: Record<string, unknown>,\n resolveMessage: GrFormMessageResolver,\n): Promise<string | undefined> {\n for (const rule of rules) {\n const empty = isEmpty(value)\n\n if (rule.required && empty) return resolveMessage('required', rule, {})\n // Необязательное пустое значение — остальные проверки этого правила пропускаем.\n if (empty) continue\n\n if (rule.type === 'email' && !EMAIL_RE.test(String(value))) return resolveMessage('email', rule, {})\n if (rule.type === 'url' && !isUrl(String(value))) return resolveMessage('url', rule, {})\n\n if (rule.len != null && measure(value) !== rule.len) return resolveMessage('len', rule, { len: rule.len })\n if (rule.min != null && measure(value) < rule.min) return resolveMessage('min', rule, { min: rule.min })\n if (rule.max != null && measure(value) > rule.max) return resolveMessage('max', rule, { max: rule.max })\n\n if (rule.pattern && !rule.pattern.test(String(value))) return resolveMessage('pattern', rule, {})\n\n if (rule.file) {\n const issue = await runFileRule(value, rule.file)\n if (issue) return resolveMessage('file', rule, { issue })\n }\n\n if (rule.validator) {\n const result = await rule.validator(value, model)\n if (result === false) return rule.message ?? resolveMessage('invalid', rule, {})\n if (typeof result === 'string') return result\n }\n }\n\n return undefined\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation-5HhpanPP.js","names":[],"sources":["../../src/components/GrForm/fileRule.ts","../../src/components/GrForm/validation.ts"],"sourcesContent":["/**\n * Файловое правило `GrForm` — мост между декларативными `rules` и валидаторами\n * из `src/fileValidation`.\n *\n * Своих проверок здесь нет ни одной: правило собирает те же валидаторы, которые\n * `GrFormFile` и `v-dropzone` запускают на выборе и на drop. Поэтому ограничение,\n * переехавшее из пропов поля в правило формы, не меняет ни поведения, ни текста\n * ошибки — меняется только момент проверки и то, кто о ней сообщает.\n *\n * Модуль чистый (без Vue): его гоняют юнит-тесты без монтирования.\n */\n\nimport {\n acceptValidator,\n allowedExtensionsValidator,\n allowedMimeTypesValidator,\n maxCountValidator,\n maxFileSize,\n maxTotalSizeBytesValidator,\n runFileValidators,\n type FileValidationIssue,\n type FileValidator,\n} from '../../fileValidation'\n\nexport interface GrFormFileRule {\n /** W3C `accept`: `'image/*,.pdf'`. */\n accept?: string\n /** Белый список расширений (с точкой или без). */\n extensions?: string[]\n /** Белый список MIME-типов. */\n mimeTypes?: string[]\n /** Предел размера одного файла в мегабайтах. */\n maxSizeMb?: number\n /** Предел размера одного файла в байтах. Задан вместе с `maxSizeMb` — берётся меньший. */\n maxSizeBytes?: number\n /** Максимум файлов в наборе. */\n maxCount?: number\n /** Суммарный размер набора в мегабайтах. */\n maxTotalSizeMb?: number\n /** Свои валидаторы — тот же тип, что у `GrFormFile` и `v-dropzone`. Идут после встроенных. */\n validators?: FileValidator[]\n}\n\nconst BYTES_IN_MB = 1024 * 1024\n\nfunction mbToBytes(mb: number | undefined): number | undefined {\n return typeof mb === 'number' && Number.isFinite(mb) ? mb * BYTES_IN_MB : undefined\n}\n\nfunction isFile(value: unknown): value is File {\n return typeof File !== 'undefined' && value instanceof File\n}\n\n/**\n * Значение поля как набор файлов. Всё, что файлами не является, даёт пустой\n * набор: правило молчит, а пустоту разбирает `required` движка.\n */\nexport function fileRuleFiles(value: unknown): File[] {\n if (isFile(value)) return [value]\n if (Array.isArray(value)) return value.filter(isFile)\n return []\n}\n\n/** Порядок повторяет `GrFormFile`: встроенные проверки, затем валидаторы потребителя. */\nexport function fileRuleValidators(rule: GrFormFileRule): FileValidator[] {\n return [\n acceptValidator(rule.accept),\n ...(rule.extensions ? [allowedExtensionsValidator(rule.extensions)] : []),\n ...(rule.mimeTypes ? [allowedMimeTypesValidator(rule.mimeTypes)] : []),\n maxFileSize({ bytes: rule.maxSizeBytes, mb: rule.maxSizeMb }),\n maxCountValidator(rule.maxCount),\n maxTotalSizeBytesValidator(mbToBytes(rule.maxTotalSizeMb)),\n ...(rule.validators ?? []),\n ]\n}\n\n/**\n * Первая проблема набора или `undefined`. Первая, а не все: у поля формы одна\n * строка ошибки, и файлы тут не исключение — подробный разбор по каждому файлу\n * остаётся за `GrFormFile`.\n */\nexport async function runFileRule(\n value: unknown,\n rule: GrFormFileRule,\n): Promise<FileValidationIssue | undefined> {\n const files = fileRuleFiles(value)\n if (!files.length) return undefined\n\n const { issues } = await runFileValidators(files, fileRuleValidators(rule), {\n source: 'form',\n multiple: Array.isArray(value),\n accept: rule.accept,\n })\n\n return issues[0]\n}\n","/**\n * Движок валидации `GrForm` — чистый (без Vue/i18n), тестируется изолированно.\n *\n * Правило описывает набор проверок значения поля; `runFieldRules` прогоняет их по\n * порядку и возвращает первое сообщение об ошибке (или `undefined`, если валидно).\n * Сообщения резолвит потребитель (компонент) через `resolveMessage` — так i18n и\n * дефолтные тексты остаются в компоненте, а движок ничего о них не знает.\n */\n\nimport { resolveFileValidationMessage, type FileValidationIssue } from '../../fileValidation'\nimport { runFileRule, type GrFormFileRule } from './fileRule'\n\nexport type { GrFormFileRule } from './fileRule'\n\nexport type GrFormTrigger = 'blur' | 'change' | 'submit'\n\n/** Результат кастомного валидатора: `true`/void — ок; `false` — ошибка (дефолт-текст); строка — текст ошибки. */\nexport type GrFormValidatorResult = boolean | string | void\n\nexport interface GrFormRule {\n /** Значение обязательно (не пустое). */\n required?: boolean\n /** Готовый текст ошибки — перекрывает дефолтное сообщение любой проверки этого правила. */\n message?: string\n /** Минимум: длина для строки/массива, значение для числа. */\n min?: number\n /** Максимум: длина для строки/массива, значение для числа. */\n max?: number\n /** Точная длина/значение. */\n len?: number\n /** Регулярное выражение (проверяется по `String(value)`). */\n pattern?: RegExp\n /** Встроенный тип-валидатор. */\n type?: 'email' | 'url'\n /** Ограничения для значения `File | File[]`: тип, размер, количество. */\n file?: GrFormFileRule\n /** Кастомный (в т.ч. async) валидатор. Второй аргумент — весь `model`. */\n validator?: (value: unknown, model: Record<string, unknown>) => GrFormValidatorResult | Promise<GrFormValidatorResult>\n /** Когда правило срабатывает. По умолчанию — на любом триггере (blur/change/submit). */\n trigger?: GrFormTrigger | GrFormTrigger[]\n}\n\nexport type GrFormRules = Record<string, GrFormRule | GrFormRule[]>\n\n/** Вид сработавшей проверки — ключ для дефолтного i18n-сообщения. */\nexport type GrFormRuleFailure = 'required' | 'min' | 'max' | 'len' | 'pattern' | 'email' | 'url' | 'file' | 'invalid'\n\nexport type GrFormMessageResolver = (\n kind: GrFormRuleFailure,\n rule: GrFormRule,\n params: Record<string, unknown>,\n) => string\n\n/** Подпись `t()` из `useGranularityTranslations()`: ключ, фолбэк, параметры. */\nexport type GrFormMessageTranslate = (\n key: string,\n fallback: string,\n params?: Record<string, unknown>,\n) => string\n\nconst DEFAULT_MESSAGES: Record<GrFormRuleFailure, string> = {\n required: 'This field is required',\n min: 'Minimum is {min}',\n max: 'Maximum is {max}',\n len: 'Length must be {len}',\n pattern: 'Invalid format',\n email: 'Enter a valid email',\n url: 'Enter a valid URL',\n file: 'Invalid file',\n invalid: 'Invalid value',\n}\n\n/**\n * Дефолтный резолвер сообщений: `rule.message` сильнее всего, иначе ключ\n * `gr.form.<kind>` из локали, иначе английский фолбэк.\n *\n * Вынесен из `GrForm.vue` и экспортируется наружу не «на всякий случай»:\n * `runFieldRules` публичен, но без резолвера бесполезен — любой, кто хочет\n * прогнать те же правила вне формы, обязан был написать свой.\n */\nexport function createGrFormMessageResolver(t: GrFormMessageTranslate): GrFormMessageResolver {\n return (kind, rule, params) => {\n if (rule.message) return rule.message\n\n // Текст файловой ошибки приходит от самого валидатора и локализуется его же\n // ключом (`gr.fileValidation.*`): иначе одно и то же «файл не того типа»\n // звучало бы по-разному в поле и в форме. `gr.form.file` — фолбэк на случай\n // вида `file` без issue: сюда движок не приходит, но резолвер публичный.\n const issue = params.issue as FileValidationIssue | undefined\n if (kind === 'file' && issue) return resolveFileValidationMessage(issue, t)\n\n return t(`gr.form.${kind}`, DEFAULT_MESSAGES[kind], params)\n }\n}\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isUrl(value: string): boolean {\n return URL.canParse(value)\n}\n\n/** Пустое значение: `null`/`undefined`/пустая (или пробельная) строка/пустой массив. */\nexport function isEmpty(value: unknown): boolean {\n if (value == null) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n return false\n}\n\n/** Мера значения для min/max/len: число → само значение, строка/массив → длина. */\nfunction measure(value: unknown): number {\n if (typeof value === 'number') return value\n if (typeof value === 'string') return value.length\n if (Array.isArray(value)) return value.length\n return Number(value)\n}\n\n/** Достаёт значение из объекта по dot-path (`address.city`). */\nexport function getByPath(obj: Record<string, unknown>, path: string): unknown {\n if (!path.includes('.')) return obj[path]\n return path.split('.').reduce<unknown>((acc, key) => {\n if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[key]\n return undefined\n }, obj)\n}\n\n/** Записывает значение в объект по dot-path (создаёт промежуточные объекты). */\nexport function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split('.')\n const last = keys.pop() as string\n let target = obj\n for (const key of keys) {\n if (!target[key] || typeof target[key] !== 'object') target[key] = {}\n target = target[key] as Record<string, unknown>\n }\n target[last] = value\n}\n\n/** Нормализует правило(а) поля в массив. */\nexport function toRuleArray(rules: GrFormRule | GrFormRule[] | undefined): GrFormRule[] {\n if (!rules) return []\n return Array.isArray(rules) ? rules : [rules]\n}\n\nfunction ruleMatchesTrigger(rule: GrFormRule, trigger?: GrFormTrigger): boolean {\n if (!trigger) return true // полная валидация (validate()) — все правила\n if (!rule.trigger) return true // правило без триггера срабатывает на любом\n const triggers = Array.isArray(rule.trigger) ? rule.trigger : [rule.trigger]\n return triggers.includes(trigger)\n}\n\n/** Правила поля, применимые для данного триггера (undefined = все). */\nexport function rulesForTrigger(rules: GrFormRule[], trigger?: GrFormTrigger): GrFormRule[] {\n return rules.filter(rule => ruleMatchesTrigger(rule, trigger))\n}\n\n/** Есть ли среди правил `required` (для маркера обязательности в поле). */\nexport function rulesRequired(rules: GrFormRule[]): boolean {\n return rules.some(rule => rule.required === true)\n}\n\n/**\n * Прогоняет правила по порядку, возвращает первое сообщение об ошибке или `undefined`.\n * Пустое необязательное значение проходит все проверки (кроме `required`).\n */\nexport async function runFieldRules(\n value: unknown,\n rules: GrFormRule[],\n model: Record<string, unknown>,\n resolveMessage: GrFormMessageResolver,\n): Promise<string | undefined> {\n for (const rule of rules) {\n const empty = isEmpty(value)\n\n if (rule.required && empty) return resolveMessage('required', rule, {})\n // Необязательное пустое значение — остальные проверки этого правила пропускаем.\n if (empty) continue\n\n if (rule.type === 'email' && !EMAIL_RE.test(String(value))) return resolveMessage('email', rule, {})\n if (rule.type === 'url' && !isUrl(String(value))) return resolveMessage('url', rule, {})\n\n if (rule.len != null && measure(value) !== rule.len) return resolveMessage('len', rule, { len: rule.len })\n if (rule.min != null && measure(value) < rule.min) return resolveMessage('min', rule, { min: rule.min })\n if (rule.max != null && measure(value) > rule.max) return resolveMessage('max', rule, { max: rule.max })\n\n if (rule.pattern && !rule.pattern.test(String(value))) return resolveMessage('pattern', rule, {})\n\n if (rule.file) {\n const issue = await runFileRule(value, rule.file)\n if (issue) return resolveMessage('file', rule, { issue })\n }\n\n if (rule.validator) {\n const result = await rule.validator(value, model)\n if (result === false) return rule.message ?? resolveMessage('invalid', rule, {})\n if (typeof result === 'string') return result\n }\n }\n\n return undefined\n}\n"],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@ import { t as ensurePortalRoot } from "../../../chunks/portalRoot-CVhCgdG6.js";
|
|
|
5
5
|
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-DCz-Wn-V.js";
|
|
6
6
|
import { n as coreResponseErrorParsers, o as responseErrorParserPresets, r as createResponseErrorClassifier } from "../../GrResponseErrorBanner/chunks/GrResponseErrorBanner-BhP-xw2P.js";
|
|
7
7
|
import { t as GrConfirmDialog_default } from "../../GrConfirmDialog/chunks/GrConfirmDialog-CwXNLyzk.js";
|
|
8
|
-
import { t as GrPromptDialog_default } from "../../GrPromptDialog/chunks/GrPromptDialog-
|
|
8
|
+
import { t as GrPromptDialog_default } from "../../GrPromptDialog/chunks/GrPromptDialog-BGuMUzlv.js";
|
|
9
9
|
import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, getCurrentInstance, inject, markRaw, mergeProps, nextTick, onBeforeUnmount, onMounted, openBlock, provide, reactive, ref, render, renderList, shallowRef, toDisplayString, withCtx } from "vue";
|
|
10
10
|
//#region src/components/GrDialogService/store.ts
|
|
11
11
|
function createDialogServiceState() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrDialogService-CHhzGRWy.js","names":[],"sources":["../../../../src/components/GrDialogService/store.ts","../../../../src/components/GrDialogService/useDialogService.ts","../../../../src/components/GrDialogService/GrDialogServiceItem.vue","../../../../src/components/GrDialogService/GrDialogServiceItem.vue","../../../../src/components/GrDialogService/GrDialogServiceHost.vue","../../../../src/components/GrDialogService/GrDialogServiceHost.vue"],"sourcesContent":["import { reactive } from 'vue'\nimport type { AppContext, Raw } from 'vue'\n\nimport type { GrConfigContext } from '../GrConfigProvider/context'\nimport type { GranularityI18nLike } from '../../internal/granularityI18n'\n\nimport type {\n DialogBaseOptions,\n DialogKind,\n DialogOnConfirm,\n DialogResult,\n} from './types'\n\n/**\n * Внутреннее описание одного запроса диалога в очереди. Создаётся методами\n * сервиса (`confirm`/`alert`/`prompt`/`open`) и потребляется хостом\n * (`GrDialogServiceHost`), который рендерит видимую часть очереди.\n */\nexport interface DialogRequest {\n id: string\n kind: DialogKind\n message: string\n options: DialogBaseOptions\n onConfirm?: DialogOnConfirm<any>\n /** Позиция среди ожидающих: больше — раньше. При равенстве порядок FIFO. */\n priority: number\n /**\n * Заявка создана из `onConfirm` уже открытого диалога и показывается **поверх**\n * него, а не за ним. Без этого вложенный вызов вставал бы в очередь за тем,\n * кто его ждёт: внешнее окно висело бы в загрузке, а промис не резолвился бы\n * никогда.\n */\n nested: boolean\n /**\n * Заявка уже завершена. Флаг живёт на самой заявке, а не на хосте: завершить\n * её могут трое (кнопка в окне, `close()` промиса, `closeAll()`), и общее\n * состояние на хосте означало бы, что второй и третий путь про первый не\n * знают.\n */\n settled: boolean\n /** Резолвит промис вызова детальным результатом `{ action, value }`. */\n resolve: (result: DialogResult<any>) => void\n /**\n * Конфиг и i18n, захваченные в месте вызова сервиса (в `setup`, где вызывающий\n * компонент ещё находится внутри дерева провайдера). Хост монтируется в\n * `document.body` вне дерева, поэтому сам их не увидит — см.\n * `SPEC-GrConfig-resolver.md`.\n *\n * Лежат в запросе, а не в состоянии сервиса: два поддерева с разными\n * провайдерами должны получать каждое свой конфиг.\n *\n * `Raw<…>` обязателен: очередь — `reactive`, а он разворачивает вложенные рефы.\n * Без `markRaw` контекст превратился бы в объект значений, `size.value` стало\n * бы `undefined`, и мост молча перестал бы работать.\n */\n config?: Raw<GrConfigContext> | null\n i18n?: Raw<GranularityI18nLike> | null\n}\n\n/** Контекст, захваченный вызовом `useDialogService()` внутри `setup`. */\nexport interface CapturedDialogContext {\n config: Raw<GrConfigContext> | null\n i18n: Raw<GranularityI18nLike> | null\n}\n\n/**\n * Состояние одного инстанса сервиса: очередь, смонтированный хост и захваченный\n * контекст приложения. Живёт либо в приложении (через\n * `granularityDialogServicePlugin`), либо в ленивом модульном фолбэке для\n * простых SPA — см. `useDialogService.ts`.\n */\nexport interface DialogServiceState {\n /** Очередь заявок. Видимую часть выбирает хост, остальные ждут. */\n queue: DialogRequest[]\n mounted: boolean\n container: HTMLElement | null\n appContext: AppContext | null\n lastCapturedContext: CapturedDialogContext | null\n contextlessWarned: boolean\n /**\n * Заявки, чей `onConfirm` сейчас в полёте, от внешней к самой глубокой.\n * По ним определяется вложенность нового вызова.\n */\n inFlight: DialogRequest[]\n}\n\nexport function createDialogServiceState(): DialogServiceState {\n return {\n queue: reactive<DialogRequest[]>([]),\n mounted: false,\n container: null,\n appContext: null,\n lastCapturedContext: null,\n contextlessWarned: false,\n inFlight: [],\n }\n}\n\nlet counter = 0\n\nexport function makeDialogId(): string {\n counter += 1\n return `gr-dialog-${Date.now()}-${counter}`\n}\n\n/**\n * Видимая часть очереди: голова и непрерывный хвост вложенных заявок за ней.\n * Всё остальное ждёт — обычные вызовы по-прежнему идут по одному, чтобы три\n * алерта из цикла не легли стопкой.\n */\nexport function visibleDialogRequests(state: DialogServiceState): DialogRequest[] {\n const visible: DialogRequest[] = []\n\n for (const request of state.queue) {\n if (visible.length === 0 || request.nested)\n visible.push(request)\n else\n break\n }\n\n return visible\n}\n\n/**\n * Ставит заявку в очередь. Единственное место, где решается её позиция:\n *\n * - вложенная (создана, пока `onConfirm` другой заявки в полёте) встаёт сразу\n * за своим родителем и показывается поверх него;\n * - обычная — перед всеми ожидающими с меньшим приоритетом; показанные окна не\n * трогаются, выдёргивать фокус-ловушку из-под пользователя нельзя.\n */\nexport function enqueueDialogRequest(state: DialogServiceState, request: DialogRequest): void {\n const parent = state.inFlight[state.inFlight.length - 1]\n\n if (parent) {\n request.nested = true\n const parentIndex = state.queue.findIndex(item => item.id === parent.id)\n state.queue.splice(parentIndex + 1, 0, request)\n return\n }\n\n const visibleCount = visibleDialogRequests(state).length\n const insertAt = state.queue.findIndex(\n (item, index) => index >= visibleCount && item.priority < request.priority,\n )\n\n if (insertAt < 0)\n state.queue.push(request)\n else\n state.queue.splice(insertAt, 0, request)\n}\n\n/**\n * Заявка входит в «полёт»: пока её `onConfirm` не вернулся, новые вызовы\n * считаются вложенными в неё.\n */\nexport function startDialogInFlight(state: DialogServiceState, request: DialogRequest): void {\n state.inFlight.push(request)\n}\n\n/** Заявка вышла из полёта (колбэк вернулся или окно размонтировано). */\nexport function finishDialogInFlight(state: DialogServiceState, request: DialogRequest): void {\n const index = state.inFlight.indexOf(request)\n if (index >= 0)\n state.inFlight.splice(index, 1)\n}\n\n/**\n * Единственный способ завершить заявку: идемпотентно резолвит промис и снимает\n * её из очереди.\n *\n * `removeFromQueue: false` нужен окну: оно гасит себя и даёт анимации закрытия\n * доиграть, прежде чем заявка исчезнет из очереди. Корректность от этой\n * отсрочки не зависит: повторное завершение отсекается флагом на заявке.\n */\nexport function settleDialogRequest(\n state: DialogServiceState,\n request: DialogRequest,\n result: DialogResult<any>,\n { removeFromQueue = true }: { removeFromQueue?: boolean } = {},\n): boolean {\n if (request.settled)\n return false\n\n request.settled = true\n request.resolve(result)\n\n if (removeFromQueue)\n removeDialogRequest(state, request)\n\n return true\n}\n\n/** Снимает заявку из очереди, если она ещё там. */\nexport function removeDialogRequest(state: DialogServiceState, request: DialogRequest): void {\n const index = state.queue.findIndex(item => item.id === request.id)\n if (index >= 0)\n state.queue.splice(index, 1)\n}\n","import { createVNode, getCurrentInstance, inject, markRaw, render } from 'vue'\nimport type { App, AppContext, InjectionKey } from 'vue'\n\nimport { ensurePortalRoot } from '../../composables/internal/portalRoot'\nimport GrDialogServiceHost from './GrDialogServiceHost.vue'\nimport { useGrConfig } from '../GrConfigProvider/context'\nimport { resolveGranularityI18n } from '../../internal/granularityI18n'\nimport {\n createDialogServiceState,\n enqueueDialogRequest,\n makeDialogId,\n settleDialogRequest,\n} from './store'\nimport type { CapturedDialogContext, DialogRequest, DialogServiceState } from './store'\nimport type {\n DialogAlertOptions,\n DialogBaseOptions,\n DialogConfirmOptions,\n DialogKind,\n DialogOnConfirm,\n DialogPromise,\n DialogPromptOptions,\n DialogResult,\n DialogService,\n DialogServiceDefaults,\n} from './types'\n\n/**\n * `useDialogService` — императивный сервис диалогов (в духе Element Plus\n * `ElMessageBox`). Позволяет вызывать `confirm`/`alert`/`prompt` из `<script>`\n * или `.ts` без вставки компонента в шаблон.\n *\n * Поверх существующих `GrConfirmDialog`/`GrPromptDialog`: лениво монтирует\n * единый хост (`GrDialogServiceHost`) в `document.body`, наследуя контекст\n * приложения (i18n / тема / `granular-provider`).\n */\n\n/** Ключ provide/inject для app-scoped состояния сервиса (устанавливает плагин ниже). */\nexport const GRANULARITY_DIALOG_SERVICE_STATE: InjectionKey<DialogServiceState>\n = Symbol.for('@feugene/granularity/dialog-service-state')\n\n/**\n * Инстансы, заведённые плагином. Реестр нужен готовому синглтону\n * `dialogService`: он создаётся на импорте модуля, где `inject` не работает\n * вовсе, и сам до app-scoped состояния не дотянется. Ровно один\n * зарегистрированный — ответ однозначен; несколько — выбирать за пользователя\n * нечего, и он получает предупреждение с модульным фолбэком.\n */\nconst registeredStates = new Set<DialogServiceState>()\n\n/**\n * Vue-плагин: даёт каждому приложению собственное состояние сервиса и снимает\n * его хост вместе с приложением. Обязателен для нескольких Vue-приложений на\n * одной странице (микрофронтенды — иначе они делят одну очередь и один хост) и\n * для HMR, где контейнер прошлого приложения иначе остаётся в `document.body`.\n *\n * ```ts\n * app.use(granularityDialogServicePlugin)\n * ```\n */\nexport const granularityDialogServicePlugin = {\n install(app: App) {\n const state = createDialogServiceState()\n\n app.provide(GRANULARITY_DIALOG_SERVICE_STATE, state)\n registeredStates.add(state)\n\n app.onUnmount(() => {\n teardownDialogServiceState(state)\n registeredStates.delete(state)\n })\n },\n}\n\n// Ленивый модульный фолбэк — канонический вариант для простого SPA без плагина.\nlet moduleState: DialogServiceState | null = null\n\nfunction getModuleState(): DialogServiceState {\n if (!moduleState)\n moduleState = createDialogServiceState()\n\n return moduleState\n}\n\nlet ambiguousStateWarned = false\n\nfunction warnAmbiguousState(): void {\n if (ambiguousStateWarned) return\n if (!__GR_DEV__) return\n\n ambiguousStateWarned = true\n console.warn(\n '[granularity] useDialogService: плагин установлен в нескольких приложениях, '\n + 'а сервис запрошен вне `setup` — какое из них имелось в виду, знать неоткуда. '\n + 'Используется модульное состояние. Вызовите `useDialogService()` внутри `setup` нужного приложения.',\n )\n}\n\n/**\n * Состояние текущего вызова: app-scoped (плагин) → единственный\n * зарегистрированный инстанс → модульный фолбэк.\n */\nfunction resolveDialogServiceState(): DialogServiceState {\n // `inject` работает только в `setup` — там ответ точный.\n if (getCurrentInstance()) {\n const provided = inject(GRANULARITY_DIALOG_SERVICE_STATE, null)\n if (provided) return provided\n }\n\n if (registeredStates.size === 1)\n return registeredStates.values().next().value!\n\n if (registeredStates.size > 1)\n warnAmbiguousState()\n\n return getModuleState()\n}\n\nfunction ensureMounted(state: DialogServiceState, appContext?: AppContext | null): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n\n if (appContext) state.appContext = appContext\n if (state.mounted) return\n\n const container = document.createElement('div')\n container.setAttribute('data-gr-dialog-service-host', '')\n container.setAttribute('data-gr-overlay-root', '')\n // Хост уезжает в общий портал оверлеев: он должен лежать в той же ветке, что\n // и остальные слои, иначе правило `inert` пометит его вместе со страницей.\n ;(ensurePortalRoot() ?? document.body).appendChild(container)\n state.container = container\n\n // Состояние отдаём пропом: хост монтируется вне дерева и своим `inject` до\n // app-scoped состояния не дотянулся бы. `markRaw` — чтобы Vue не пытался\n // сделать реактивной саму обёртку: реактивна очередь внутри неё.\n const vnode = createVNode(GrDialogServiceHost, { state: markRaw(state) })\n vnode.appContext = state.appContext ?? null\n render(vnode, container)\n state.mounted = true\n}\n\nfunction warnIfContextless(state: DialogServiceState, context: CapturedDialogContext | null): void {\n if (state.contextlessWarned || context?.config || context?.i18n) return\n if (!__GR_DEV__) return\n\n state.contextlessWarned = true\n console.warn(\n '[granularity] useDialogService: диалог открыт без контекста приложения — '\n + 'ни `GrConfigProvider`, ни адаптер i18n не найдены, будут использованы дефолты. '\n + 'Вызовите `useDialogService()` внутри `setup` хотя бы один раз или передайте `setAppContext`.',\n )\n}\n\n/** Снимает смонтированный хост и сбрасывает захваченный контекст одного инстанса. */\nfunction teardownDialogServiceState(state: DialogServiceState): void {\n if (state.container) {\n render(null, state.container)\n state.container.remove()\n state.container = null\n }\n state.mounted = false\n // Сбрасываем и кэш контекста приложения: без этого он переживает teardown и\n // протекает в следующее приложение (в тестах — в следующий тест), из-за чего\n // диалог может подхватить чужие provides и проверка соврёт.\n state.appContext = null\n state.lastCapturedContext = null\n state.contextlessWarned = false\n state.queue.splice(0, state.queue.length)\n state.inFlight.splice(0, state.inFlight.length)\n}\n\n/**\n * Тестовая/служебная очистка смонтированного хоста. Работает над тем же\n * состоянием, что и сам сервис: app-scoped при вызове из `setup` или при\n * единственном зарегистрированном приложении, иначе — модульным.\n */\nexport function teardownDialogService(): void {\n teardownDialogServiceState(resolveDialogServiceState())\n}\n\nfunction enqueue(\n state: DialogServiceState,\n kind: DialogKind,\n message: string,\n options: DialogBaseOptions,\n onConfirm: DialogOnConfirm<any> | undefined,\n captured: CapturedDialogContext,\n): { promise: Promise<DialogResult<any>>, close: () => void } {\n // Императивный сервис клиент-only: монтирует хост в `document.body`. В SSR\n // выполнять его нельзя — очередь мутировалась бы на сервере и текла между\n // запросами. Явно запрещаем вместо тихого no-op.\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n throw new Error(\n '[granularity] useDialogService is client-only and cannot be called during SSR '\n + '(it mounts a host into document.body). Guard imperative dialog calls behind a client-side check.',\n )\n }\n\n ensureMounted(state, options.appContext ?? state.appContext)\n\n const id = makeDialogId()\n let resolveFn!: (result: DialogResult<any>) => void\n const promise = new Promise<DialogResult<any>>((resolve) => {\n resolveFn = resolve\n })\n\n // Синглтон `dialogService` создаётся на импорте модуля, вне `setup`, и своего\n // контекста не имеет в принципе. Берём последний захваченный — иначе такие\n // вызовы навсегда остаются без i18n и без `GrConfigProvider`.\n const context = captured.config || captured.i18n ? captured : state.lastCapturedContext\n warnIfContextless(state, context)\n\n const request: DialogRequest = {\n id,\n kind,\n message,\n options: { ...options, message },\n onConfirm,\n priority: options.priority ?? 0,\n nested: false,\n settled: false,\n resolve: resolveFn,\n config: context?.config ?? null,\n i18n: context?.i18n ?? null,\n }\n\n enqueueDialogRequest(state, request)\n\n // Закрытие через промис идёт тем же путём, что и кнопка в окне: заявка сама\n // знает, завершена ли она, а окно по её исчезновению свернёт всё, что под неё\n // заведено (в т.ч. оборвёт in-flight `onConfirm` через `abort()`).\n const close = (): void => {\n settleDialogRequest(state, request, { action: 'close' })\n }\n\n return { promise, close }\n}\n\nfunction withClose<T>(promise: Promise<T>, close: () => void): DialogPromise<T> {\n const result = promise as DialogPromise<T>\n result.close = close\n return result\n}\n\nfunction mergeErrorDefaults(\n defaults: DialogServiceDefaults,\n options: DialogBaseOptions,\n): DialogBaseOptions & Pick<DialogConfirmOptions, 'cancelText'> {\n return {\n size: defaults.size,\n confirmText: defaults.confirmText,\n confirmVariant: defaults.confirmVariant,\n confirmTone: defaults.confirmTone,\n cancelText: defaults.cancelText,\n buttonSize: defaults.buttonSize,\n closeOnBackdrop: defaults.closeOnBackdrop,\n closeOnEsc: defaults.closeOnEsc,\n errorParsers: defaults.errorParsers,\n errorTexts: defaults.errorTexts,\n errorMessageKey: defaults.errorMessageKey,\n fieldLabels: defaults.fieldLabels,\n ...options,\n }\n}\n\n/**\n * Создаёт сервис диалогов. Опциональные `defaults` применяются ко всем\n * вызовам (можно переопределить в каждом конкретном вызове).\n *\n * Контекст приложения кэшируется автоматически при первом вызове внутри\n * `setup`. Для вызовов вне компонента используйте `setAppContext`.\n */\nexport function useDialogService(defaults: DialogServiceDefaults = {}): DialogService {\n const state = resolveDialogServiceState()\n\n // Авто-кэш appContext из текущего компонента (если вызвано в setup).\n const instance = getCurrentInstance()\n if (instance?.appContext && !state.appContext) {\n state.appContext = instance.appContext\n }\n\n // Конфиг и i18n захватываем здесь и только здесь: `inject` работает лишь в\n // `setup`, а хост монтируется вне дерева и сам до провайдера не дотянется.\n // Храним в замыкании сервиса, а не в состоянии, — иначе два поддерева с\n // разными провайдерами делили бы конфиг первого.\n // i18n захватываем не «на всякий случай»: дочерний `GrConfirmDialog` сам зовёт\n // `useGranularityTranslations()`, но его `inject` из хоста уходит в\n // `appContext.provides` — то есть видит только установку через `app.use()`.\n // Адаптер, переданный пропом `<GrConfigProvider i18n>`, живёт в дереве, и без\n // захвата диалог откатился бы на встроенные английские строки.\n //\n // `markRaw` — потому что дальше контекст ложится в `reactive`-очередь, а она\n // разворачивает вложенные рефы (см. комментарий в `store.ts`).\n const capturedI18n = instance ? resolveGranularityI18n() : null\n const captured: CapturedDialogContext = {\n config: instance ? markRaw(useGrConfig()) : null,\n i18n: capturedI18n ? markRaw(capturedI18n) : null,\n }\n\n if (captured.config || captured.i18n)\n state.lastCapturedContext = captured\n\n function confirm(message: string, options: DialogConfirmOptions = {}): DialogPromise<boolean> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(state, 'confirm', message, merged, options.onConfirm, captured)\n return withClose(promise.then(r => r.action === 'confirm'), close)\n }\n\n function alert(message: string, options: DialogAlertOptions = {}): DialogPromise<void> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(state, 'alert', message, merged, options.onConfirm, captured)\n return withClose(promise.then(() => undefined), close)\n }\n\n function prompt(message: string, options: DialogPromptOptions = {}): DialogPromise<string | null> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(state, 'prompt', message, merged, options.onConfirm, captured)\n return withClose(\n promise.then(r => (r.action === 'confirm' ? ((r.value as string) ?? '') : null)),\n close,\n )\n }\n\n function open<V = unknown>(\n kind: DialogKind,\n message: string,\n options: DialogBaseOptions = {},\n ): DialogPromise<DialogResult<V>> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(\n state,\n kind,\n message,\n merged,\n (options as DialogConfirmOptions).onConfirm,\n captured,\n )\n return withClose(promise as Promise<DialogResult<V>>, close)\n }\n\n // Порядок разбора — FIFO, как и порядок показа: `pop()` резолвил промисы\n // задом наперёд, и вызывающий получал результаты в обратном порядке.\n function closeAll(): void {\n for (const request of [...state.queue])\n settleDialogRequest(state, request, { action: 'close' })\n }\n\n function setAppContext(ctx: AppContext | null): void {\n state.appContext = ctx\n }\n\n return { confirm, alert, prompt, open, closeAll, setAppContext }\n}\n\n/** Готовый синглтон-сервис с дефолтными настройками. */\nexport const dialogService: DialogService = {\n confirm: (message, options) => useDialogService().confirm(message, options),\n alert: (message, options) => useDialogService().alert(message, options),\n prompt: (message, options) => useDialogService().prompt(message, options),\n open: (kind, message, options) => useDialogService().open(kind, message, options),\n closeAll: () => useDialogService().closeAll(),\n setAppContext: ctx => useDialogService().setAppContext(ctx),\n}\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceItem — одно окно императивного сервиса. Внутренний компонент,\n * в шаблонах напрямую не используется.\n *\n * Всё состояние живёт здесь, а не на хосте, ровно потому, что окон может быть\n * несколько: вложенный вызов из `onConfirm` показывается поверх родителя, и\n * общий `loading` с общей ошибкой означали бы, что верхнее окно пишет в нижнее.\n */\nimport { computed, inject, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrConfirmDialog from '../GrConfirmDialog/GrConfirmDialog.vue'\nimport GrPromptDialog from '../GrPromptDialog/GrPromptDialog.vue'\nimport {\n coreResponseErrorParsers,\n createResponseErrorClassifier,\n responseErrorParserPresets,\n} from '../GrResponseErrorBanner/parsers'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nimport { GR_CONFIG_KEY, type GrComponentDefaults, type GrConfigContext } from '../GrConfigProvider/context'\nimport { GRANULARITY_I18N_KEY } from '../../i18n/adapter'\nimport { resolveGranularityI18n, useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { finishDialogInFlight, removeDialogRequest, settleDialogRequest, startDialogInFlight } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\nimport type { DialogCloseAction, DialogConfirmContext, DialogConfirmOptions, DialogPromptOptions } from './types'\n\nconst props = defineProps<{\n request: DialogRequest\n}>()\n\n// Состояние инстанса приходит через `provide` хоста, а не пропом: окно его\n// мутирует (очередь, список заявок в полёте), а мутировать проп — значит писать\n// в чужое владение.\nconst state = inject(GRANULARITY_DIALOG_SERVICE_STATE) as DialogServiceState\n\n// ————— Мост конфига и i18n.\n//\n// Окно рендерится через `render(vnode, container)` в `document.body`, то есть вне\n// дерева компонентов. Vue в этом случае берёт `provides` только из `appContext`,\n// куда попадает лишь `app.provide()` — значения от `<GrConfigProvider>` до сюда\n// не доходят. Поэтому вызов сервиса захватывает их в `setup` и кладёт в заявку,\n// а окно раздаёт вниз конфиг **своей** заявки.\nconst EMPTY_DEFAULTS: GrComponentDefaults = Object.freeze({})\n\nprovide<GrConfigContext>(GR_CONFIG_KEY, {\n size: computed(() => props.request.config?.size.value),\n componentDefaults: computed(() => props.request.config?.componentDefaults.value ?? EMPTY_DEFAULTS),\n})\n\n// i18n: тот же приём. Адаптер хоста (из `appContext`) — запасной вариант для\n// вызова без захваченного: без него установка через `app.use()` осталась бы\n// незамеченной.\nconst hostI18n = resolveGranularityI18n()\n\nprovide(GRANULARITY_I18N_KEY, {\n t: (key: string, params?: Record<string, unknown>) => {\n const adapter = props.request.i18n ?? hostI18n\n // Вернуть сам ключ — принятый в пакете сигнал «перевода нет»: потребитель\n // подставит свой fallback-текст (см. `useGranularityTranslations`).\n return adapter?.t(key, params) ?? key\n },\n get locale() {\n return (props.request.i18n ?? hostI18n)?.locale\n },\n})\n\n// Подвал alert-ветки рисуется здесь, поэтому и перевод кнопки достаётся здесь.\n// Адаптер берём тот же, что раздан вниз: собственный `provide` компонента его\n// же `inject` не видит, и `useGranularityTranslations()` без аргумента ушёл бы\n// мимо захваченного вызовом адаптера — в фолбэк из `appContext`.\nconst alertConfirmText = computed(() => {\n const { t } = useGranularityTranslations(props.request.i18n ?? hostI18n)\n return t('gr.dialog.ok', 'OK')\n})\n\nconst open = ref(false)\nconst loading = ref(false)\nconst currentError = shallowRef<ResponseErrorInfo | null>(null)\n/**\n * Ошибки по именам полей. Карта, а не одна строка: `setFieldError(field, …)`\n * обещает адресность, и обещание должно быть правдой ещё до того, как в\n * диалоге появится второе поле.\n */\nconst fieldErrors = ref<Record<string, string>>({})\nconst promptValue = ref(\n props.request.options && 'value' in props.request.options\n ? ((props.request.options as { value?: string }).value ?? '')\n : '',\n)\n\n/** Единственное поле `GrPromptDialog` называется `value`. */\nconst PROMPT_FIELD = 'value'\n\n// Одна запись — её и показываем: адрес не важен, когда адресат один.\nconst promptFieldError = computed<string | null>(() => {\n const own = fieldErrors.value[PROMPT_FIELD]\n if (own != null) return own\n\n const entries = Object.values(fieldErrors.value)\n return entries.length === 1 ? entries[0] : null\n})\n\nconst hasFieldError = computed(() => Object.keys(fieldErrors.value).length > 0)\n\nconst controller = new AbortController()\nlet externalAbortCleanup: (() => void) | null = null\n\nfunction buildClassifier() {\n const opts = props.request.options\n const parsers = typeof opts.errorParsers === 'function'\n ? opts.errorParsers(responseErrorParserPresets)\n : opts.errorParsers ?? coreResponseErrorParsers\n\n return createResponseErrorClassifier({\n parsers,\n texts: opts.errorTexts,\n messageKey: opts.errorMessageKey,\n })\n}\n\nonMounted(() => {\n // Внешний AbortSignal закрывает диалог как `close`.\n const sig = props.request.options.signal\n if (sig) {\n if (sig.aborted) {\n finish('close')\n return\n }\n const onAbort = () => finish('close')\n sig.addEventListener('abort', onAbort)\n externalAbortCleanup = () => sig.removeEventListener('abort', onAbort)\n }\n\n void nextTick(() => {\n open.value = true\n })\n})\n\nonBeforeUnmount(() => {\n // Снимаем всё, что заведено под заявку: подписку на внешний сигнал и свой\n // контроллер. Размонтирование — единственная точка, которую видят все три\n // пути завершения (кнопка, `close()` промиса, `closeAll()`), поэтому обрыв\n // in-flight `onConfirm` живёт именно здесь.\n externalAbortCleanup?.()\n externalAbortCleanup = null\n controller.abort()\n\n finishDialogInFlight(state, props.request)\n})\n\nfunction finish(action: DialogCloseAction, value?: unknown): void {\n // Снятие заявки из очереди откладываем на тик: окно должно успеть закрыться\n // до того, как исчезнет из разметки.\n if (!settleDialogRequest(state, props.request, { action, value }, { removeFromQueue: false }))\n return\n\n open.value = false\n void nextTick(() => removeDialogRequest(state, props.request))\n}\n\nfunction buildContext(value: unknown): DialogConfirmContext<any> & { _errorSet: () => boolean } {\n let errorSet = false\n\n const setError = (message: string | null): void => {\n if (message == null) {\n currentError.value = null\n return\n }\n errorSet = true\n currentError.value = { kind: 'unknown', message, raw: message }\n }\n\n const setFieldError = (field: string, message: string | null): void => {\n const next = { ...fieldErrors.value }\n if (message == null) {\n delete next[field]\n fieldErrors.value = next\n return\n }\n errorSet = true\n next[field] = message\n fieldErrors.value = next\n }\n\n const clearErrors = (): void => {\n currentError.value = null\n fieldErrors.value = {}\n }\n\n const setRawError = async (raw: unknown, meta?: Record<string, unknown>): Promise<ResponseErrorInfo | null> => {\n const info = await buildClassifier()(raw, meta)\n if (info.fieldErrors?.length) {\n const next = { ...fieldErrors.value }\n for (const entry of info.fieldErrors)\n next[entry.field] = entry.messages.join(' ')\n fieldErrors.value = next\n }\n currentError.value = info\n errorSet = true\n return info\n }\n\n return {\n value,\n signal: controller.signal,\n setError,\n setRawError,\n setFieldError,\n clearErrors,\n setLoading: (v: boolean) => { loading.value = v },\n close: (action: DialogCloseAction = 'close') => finish(action),\n _errorSet: () => errorSet,\n }\n}\n\nasync function handleConfirm(): Promise<void> {\n const req = props.request\n const value = req.kind === 'prompt' ? promptValue.value : undefined\n\n if (!req.onConfirm) {\n finish('confirm', value)\n return\n }\n\n currentError.value = null\n fieldErrors.value = {}\n loading.value = true\n\n const ctx = buildContext(value)\n\n // Пока колбэк в полёте, заявка считается родителем для новых вызовов: диалог,\n // открытый изнутри `onConfirm`, показывается поверх, а не встаёт в очередь за\n // тем, кто его ждёт.\n startDialogInFlight(state, req)\n\n try {\n const result = await req.onConfirm(ctx)\n // Сначала проверка «диалог ещё жив», потом запись состояния: за время\n // `await` заявку мог завершить `ctx.close()` или `closeAll()`.\n if (req.settled) return\n loading.value = false\n if (result === false) return // оставить открытым без ошибки\n if (currentError.value || hasFieldError.value || ctx._errorSet()) return // есть ошибка — не закрывать\n finish('confirm', value)\n }\n catch (error) {\n if (req.settled) return\n loading.value = false\n if (!ctx._errorSet()) await ctx.setRawError(error)\n // оставить диалог открытым с показанной ошибкой\n }\n finally {\n finishDialogInFlight(state, req)\n }\n}\n\nfunction handleCancel(): void {\n finish('cancel')\n}\n\nfunction handleModelUpdate(value: boolean): void {\n if (!value) finish('close')\n}\n\n// Опции веток читаются типизированно, без `as any` в шаблоне.\nconst promptOptions = computed<DialogPromptOptions>(() => props.request.options as DialogPromptOptions)\nconst confirmOptions = computed<DialogConfirmOptions>(() => props.request.options as DialogConfirmOptions)\n\n// Прокидываемые в дочерний диалог общие пропы.\nconst sharedProps = computed(() => {\n const o = props.request.options\n return {\n title: o.title,\n size: o.size,\n closeOnBackdrop: o.closeOnBackdrop,\n closeOnEsc: o.closeOnEsc,\n // Подавление «мягкого» закрытия на время `onConfirm` считает сам диалог по\n // `confirmLoading` — здесь только включаем режим, чтобы механизм жил в\n // одном месте, а не дублировался сервисом.\n persistent: true,\n showHeader: o.showHeader,\n showCloseButton: o.showCloseButton,\n headerConfig: o.headerConfig,\n footerConfig: o.footerConfig,\n bodyConfig: o.bodyConfig,\n closeLabel: o.closeLabel,\n buttonSize: o.buttonSize,\n confirmText: o.confirmText,\n confirmVariant: o.confirmVariant,\n confirmTone: o.confirmTone,\n }\n})\n</script>\n\n<template>\n <!-- prompt -->\n <GrPromptDialog\n v-if=\"request.kind === 'prompt'\"\n v-bind=\"sharedProps\"\n v-model:value=\"promptValue\"\n :model-value=\"open\"\n :description=\"request.options.message ?? request.options.description\"\n :label=\"promptOptions.label\"\n :placeholder=\"promptOptions.placeholder\"\n :required=\"promptOptions.required ?? false\"\n :required-error-text=\"promptOptions.requiredErrorText\"\n :cancel-text=\"promptOptions.cancelText\"\n :input-type=\"promptOptions.inputType\"\n :inputmode=\"promptOptions.inputmode\"\n :maxlength=\"promptOptions.maxlength\"\n :show-count=\"promptOptions.showCount\"\n :multiline=\"promptOptions.multiline\"\n :rows=\"promptOptions.rows\"\n :autosize=\"promptOptions.autosize\"\n :rules=\"promptOptions.rules\"\n :error=\"currentError\"\n :field-error=\"promptFieldError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n\n <!-- alert: confirm-диалог без кнопки Cancel -->\n <GrConfirmDialog\n v-else-if=\"request.kind === 'alert'\"\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n >\n <template #footer>\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton\n data-testid=\"gr-alert-confirm\"\n :variant=\"sharedProps.confirmVariant ?? 'primary'\"\n :tone=\"sharedProps.confirmTone ?? 'primary'\"\n :size=\"sharedProps.buttonSize\"\n :loading=\"loading\"\n @click=\"handleConfirm\"\n >\n {{ sharedProps.confirmText ?? alertConfirmText }}\n </GrButton>\n </div>\n </template>\n </GrConfirmDialog>\n\n <!-- confirm -->\n <GrConfirmDialog\n v-else\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :cancel-text=\"confirmOptions.cancelText\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceItem — одно окно императивного сервиса. Внутренний компонент,\n * в шаблонах напрямую не используется.\n *\n * Всё состояние живёт здесь, а не на хосте, ровно потому, что окон может быть\n * несколько: вложенный вызов из `onConfirm` показывается поверх родителя, и\n * общий `loading` с общей ошибкой означали бы, что верхнее окно пишет в нижнее.\n */\nimport { computed, inject, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrConfirmDialog from '../GrConfirmDialog/GrConfirmDialog.vue'\nimport GrPromptDialog from '../GrPromptDialog/GrPromptDialog.vue'\nimport {\n coreResponseErrorParsers,\n createResponseErrorClassifier,\n responseErrorParserPresets,\n} from '../GrResponseErrorBanner/parsers'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nimport { GR_CONFIG_KEY, type GrComponentDefaults, type GrConfigContext } from '../GrConfigProvider/context'\nimport { GRANULARITY_I18N_KEY } from '../../i18n/adapter'\nimport { resolveGranularityI18n, useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { finishDialogInFlight, removeDialogRequest, settleDialogRequest, startDialogInFlight } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\nimport type { DialogCloseAction, DialogConfirmContext, DialogConfirmOptions, DialogPromptOptions } from './types'\n\nconst props = defineProps<{\n request: DialogRequest\n}>()\n\n// Состояние инстанса приходит через `provide` хоста, а не пропом: окно его\n// мутирует (очередь, список заявок в полёте), а мутировать проп — значит писать\n// в чужое владение.\nconst state = inject(GRANULARITY_DIALOG_SERVICE_STATE) as DialogServiceState\n\n// ————— Мост конфига и i18n.\n//\n// Окно рендерится через `render(vnode, container)` в `document.body`, то есть вне\n// дерева компонентов. Vue в этом случае берёт `provides` только из `appContext`,\n// куда попадает лишь `app.provide()` — значения от `<GrConfigProvider>` до сюда\n// не доходят. Поэтому вызов сервиса захватывает их в `setup` и кладёт в заявку,\n// а окно раздаёт вниз конфиг **своей** заявки.\nconst EMPTY_DEFAULTS: GrComponentDefaults = Object.freeze({})\n\nprovide<GrConfigContext>(GR_CONFIG_KEY, {\n size: computed(() => props.request.config?.size.value),\n componentDefaults: computed(() => props.request.config?.componentDefaults.value ?? EMPTY_DEFAULTS),\n})\n\n// i18n: тот же приём. Адаптер хоста (из `appContext`) — запасной вариант для\n// вызова без захваченного: без него установка через `app.use()` осталась бы\n// незамеченной.\nconst hostI18n = resolveGranularityI18n()\n\nprovide(GRANULARITY_I18N_KEY, {\n t: (key: string, params?: Record<string, unknown>) => {\n const adapter = props.request.i18n ?? hostI18n\n // Вернуть сам ключ — принятый в пакете сигнал «перевода нет»: потребитель\n // подставит свой fallback-текст (см. `useGranularityTranslations`).\n return adapter?.t(key, params) ?? key\n },\n get locale() {\n return (props.request.i18n ?? hostI18n)?.locale\n },\n})\n\n// Подвал alert-ветки рисуется здесь, поэтому и перевод кнопки достаётся здесь.\n// Адаптер берём тот же, что раздан вниз: собственный `provide` компонента его\n// же `inject` не видит, и `useGranularityTranslations()` без аргумента ушёл бы\n// мимо захваченного вызовом адаптера — в фолбэк из `appContext`.\nconst alertConfirmText = computed(() => {\n const { t } = useGranularityTranslations(props.request.i18n ?? hostI18n)\n return t('gr.dialog.ok', 'OK')\n})\n\nconst open = ref(false)\nconst loading = ref(false)\nconst currentError = shallowRef<ResponseErrorInfo | null>(null)\n/**\n * Ошибки по именам полей. Карта, а не одна строка: `setFieldError(field, …)`\n * обещает адресность, и обещание должно быть правдой ещё до того, как в\n * диалоге появится второе поле.\n */\nconst fieldErrors = ref<Record<string, string>>({})\nconst promptValue = ref(\n props.request.options && 'value' in props.request.options\n ? ((props.request.options as { value?: string }).value ?? '')\n : '',\n)\n\n/** Единственное поле `GrPromptDialog` называется `value`. */\nconst PROMPT_FIELD = 'value'\n\n// Одна запись — её и показываем: адрес не важен, когда адресат один.\nconst promptFieldError = computed<string | null>(() => {\n const own = fieldErrors.value[PROMPT_FIELD]\n if (own != null) return own\n\n const entries = Object.values(fieldErrors.value)\n return entries.length === 1 ? entries[0] : null\n})\n\nconst hasFieldError = computed(() => Object.keys(fieldErrors.value).length > 0)\n\nconst controller = new AbortController()\nlet externalAbortCleanup: (() => void) | null = null\n\nfunction buildClassifier() {\n const opts = props.request.options\n const parsers = typeof opts.errorParsers === 'function'\n ? opts.errorParsers(responseErrorParserPresets)\n : opts.errorParsers ?? coreResponseErrorParsers\n\n return createResponseErrorClassifier({\n parsers,\n texts: opts.errorTexts,\n messageKey: opts.errorMessageKey,\n })\n}\n\nonMounted(() => {\n // Внешний AbortSignal закрывает диалог как `close`.\n const sig = props.request.options.signal\n if (sig) {\n if (sig.aborted) {\n finish('close')\n return\n }\n const onAbort = () => finish('close')\n sig.addEventListener('abort', onAbort)\n externalAbortCleanup = () => sig.removeEventListener('abort', onAbort)\n }\n\n void nextTick(() => {\n open.value = true\n })\n})\n\nonBeforeUnmount(() => {\n // Снимаем всё, что заведено под заявку: подписку на внешний сигнал и свой\n // контроллер. Размонтирование — единственная точка, которую видят все три\n // пути завершения (кнопка, `close()` промиса, `closeAll()`), поэтому обрыв\n // in-flight `onConfirm` живёт именно здесь.\n externalAbortCleanup?.()\n externalAbortCleanup = null\n controller.abort()\n\n finishDialogInFlight(state, props.request)\n})\n\nfunction finish(action: DialogCloseAction, value?: unknown): void {\n // Снятие заявки из очереди откладываем на тик: окно должно успеть закрыться\n // до того, как исчезнет из разметки.\n if (!settleDialogRequest(state, props.request, { action, value }, { removeFromQueue: false }))\n return\n\n open.value = false\n void nextTick(() => removeDialogRequest(state, props.request))\n}\n\nfunction buildContext(value: unknown): DialogConfirmContext<any> & { _errorSet: () => boolean } {\n let errorSet = false\n\n const setError = (message: string | null): void => {\n if (message == null) {\n currentError.value = null\n return\n }\n errorSet = true\n currentError.value = { kind: 'unknown', message, raw: message }\n }\n\n const setFieldError = (field: string, message: string | null): void => {\n const next = { ...fieldErrors.value }\n if (message == null) {\n delete next[field]\n fieldErrors.value = next\n return\n }\n errorSet = true\n next[field] = message\n fieldErrors.value = next\n }\n\n const clearErrors = (): void => {\n currentError.value = null\n fieldErrors.value = {}\n }\n\n const setRawError = async (raw: unknown, meta?: Record<string, unknown>): Promise<ResponseErrorInfo | null> => {\n const info = await buildClassifier()(raw, meta)\n if (info.fieldErrors?.length) {\n const next = { ...fieldErrors.value }\n for (const entry of info.fieldErrors)\n next[entry.field] = entry.messages.join(' ')\n fieldErrors.value = next\n }\n currentError.value = info\n errorSet = true\n return info\n }\n\n return {\n value,\n signal: controller.signal,\n setError,\n setRawError,\n setFieldError,\n clearErrors,\n setLoading: (v: boolean) => { loading.value = v },\n close: (action: DialogCloseAction = 'close') => finish(action),\n _errorSet: () => errorSet,\n }\n}\n\nasync function handleConfirm(): Promise<void> {\n const req = props.request\n const value = req.kind === 'prompt' ? promptValue.value : undefined\n\n if (!req.onConfirm) {\n finish('confirm', value)\n return\n }\n\n currentError.value = null\n fieldErrors.value = {}\n loading.value = true\n\n const ctx = buildContext(value)\n\n // Пока колбэк в полёте, заявка считается родителем для новых вызовов: диалог,\n // открытый изнутри `onConfirm`, показывается поверх, а не встаёт в очередь за\n // тем, кто его ждёт.\n startDialogInFlight(state, req)\n\n try {\n const result = await req.onConfirm(ctx)\n // Сначала проверка «диалог ещё жив», потом запись состояния: за время\n // `await` заявку мог завершить `ctx.close()` или `closeAll()`.\n if (req.settled) return\n loading.value = false\n if (result === false) return // оставить открытым без ошибки\n if (currentError.value || hasFieldError.value || ctx._errorSet()) return // есть ошибка — не закрывать\n finish('confirm', value)\n }\n catch (error) {\n if (req.settled) return\n loading.value = false\n if (!ctx._errorSet()) await ctx.setRawError(error)\n // оставить диалог открытым с показанной ошибкой\n }\n finally {\n finishDialogInFlight(state, req)\n }\n}\n\nfunction handleCancel(): void {\n finish('cancel')\n}\n\nfunction handleModelUpdate(value: boolean): void {\n if (!value) finish('close')\n}\n\n// Опции веток читаются типизированно, без `as any` в шаблоне.\nconst promptOptions = computed<DialogPromptOptions>(() => props.request.options as DialogPromptOptions)\nconst confirmOptions = computed<DialogConfirmOptions>(() => props.request.options as DialogConfirmOptions)\n\n// Прокидываемые в дочерний диалог общие пропы.\nconst sharedProps = computed(() => {\n const o = props.request.options\n return {\n title: o.title,\n size: o.size,\n closeOnBackdrop: o.closeOnBackdrop,\n closeOnEsc: o.closeOnEsc,\n // Подавление «мягкого» закрытия на время `onConfirm` считает сам диалог по\n // `confirmLoading` — здесь только включаем режим, чтобы механизм жил в\n // одном месте, а не дублировался сервисом.\n persistent: true,\n showHeader: o.showHeader,\n showCloseButton: o.showCloseButton,\n headerConfig: o.headerConfig,\n footerConfig: o.footerConfig,\n bodyConfig: o.bodyConfig,\n closeLabel: o.closeLabel,\n buttonSize: o.buttonSize,\n confirmText: o.confirmText,\n confirmVariant: o.confirmVariant,\n confirmTone: o.confirmTone,\n }\n})\n</script>\n\n<template>\n <!-- prompt -->\n <GrPromptDialog\n v-if=\"request.kind === 'prompt'\"\n v-bind=\"sharedProps\"\n v-model:value=\"promptValue\"\n :model-value=\"open\"\n :description=\"request.options.message ?? request.options.description\"\n :label=\"promptOptions.label\"\n :placeholder=\"promptOptions.placeholder\"\n :required=\"promptOptions.required ?? false\"\n :required-error-text=\"promptOptions.requiredErrorText\"\n :cancel-text=\"promptOptions.cancelText\"\n :input-type=\"promptOptions.inputType\"\n :inputmode=\"promptOptions.inputmode\"\n :maxlength=\"promptOptions.maxlength\"\n :show-count=\"promptOptions.showCount\"\n :multiline=\"promptOptions.multiline\"\n :rows=\"promptOptions.rows\"\n :autosize=\"promptOptions.autosize\"\n :rules=\"promptOptions.rules\"\n :error=\"currentError\"\n :field-error=\"promptFieldError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n\n <!-- alert: confirm-диалог без кнопки Cancel -->\n <GrConfirmDialog\n v-else-if=\"request.kind === 'alert'\"\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n >\n <template #footer>\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton\n data-testid=\"gr-alert-confirm\"\n :variant=\"sharedProps.confirmVariant ?? 'primary'\"\n :tone=\"sharedProps.confirmTone ?? 'primary'\"\n :size=\"sharedProps.buttonSize\"\n :loading=\"loading\"\n @click=\"handleConfirm\"\n >\n {{ sharedProps.confirmText ?? alertConfirmText }}\n </GrButton>\n </div>\n </template>\n </GrConfirmDialog>\n\n <!-- confirm -->\n <GrConfirmDialog\n v-else\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :cancel-text=\"confirmOptions.cancelText\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceHost — внутренний компонент-оркестратор императивного\n * `useDialogService`. Не предназначен для прямого использования в шаблонах.\n *\n * Держит раскладку очереди и ничего больше: состояние каждой заявки — загрузка,\n * ошибки, введённое значение, мост конфига и i18n — живёт в\n * `GrDialogServiceItem`, потому что окон может быть несколько.\n *\n * Видимы голова очереди и её вложенные окна (`store.visibleDialogRequests`):\n * обычные вызовы по-прежнему показываются по одному, а диалог, открытый из\n * `onConfirm` другого диалога, ложится поверх родителя — иначе он ждал бы в\n * очереди того, кто ждёт его.\n */\nimport { computed, inject, provide } from 'vue'\n\nimport GrDialogServiceItem from './GrDialogServiceItem.vue'\nimport { visibleDialogRequests } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\n\nexport interface GrDialogServiceHostProps {\n /**\n * Состояние инстанса сервиса. Приходит пропом при программном монтировании:\n * хост живёт вне дерева и своим `inject` до app-scoped состояния не дотянулся\n * бы. `inject` остаётся для случая, когда хост поставили в шаблон руками.\n */\n state?: DialogServiceState | null\n}\n\nconst props = defineProps<GrDialogServiceHostProps>()\n\nconst injected = inject(GRANULARITY_DIALOG_SERVICE_STATE, null)\n\nconst activeState = computed<DialogServiceState | null>(() => props.state ?? injected)\n\nconst visible = computed<DialogRequest[]>(() =>\n activeState.value ? visibleDialogRequests(activeState.value) : [],\n)\n\n// Окна берут состояние отсюда, а не пропом: они его мутируют (очередь, список\n// заявок в полёте), а мутировать проп — значит писать в чужое владение.\nprovide(GRANULARITY_DIALOG_SERVICE_STATE, activeState.value!)\n</script>\n\n<template>\n <GrDialogServiceItem\n v-for=\"request in visible\"\n :key=\"request.id\"\n :request=\"request\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceHost — внутренний компонент-оркестратор императивного\n * `useDialogService`. Не предназначен для прямого использования в шаблонах.\n *\n * Держит раскладку очереди и ничего больше: состояние каждой заявки — загрузка,\n * ошибки, введённое значение, мост конфига и i18n — живёт в\n * `GrDialogServiceItem`, потому что окон может быть несколько.\n *\n * Видимы голова очереди и её вложенные окна (`store.visibleDialogRequests`):\n * обычные вызовы по-прежнему показываются по одному, а диалог, открытый из\n * `onConfirm` другого диалога, ложится поверх родителя — иначе он ждал бы в\n * очереди того, кто ждёт его.\n */\nimport { computed, inject, provide } from 'vue'\n\nimport GrDialogServiceItem from './GrDialogServiceItem.vue'\nimport { visibleDialogRequests } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\n\nexport interface GrDialogServiceHostProps {\n /**\n * Состояние инстанса сервиса. Приходит пропом при программном монтировании:\n * хост живёт вне дерева и своим `inject` до app-scoped состояния не дотянулся\n * бы. `inject` остаётся для случая, когда хост поставили в шаблон руками.\n */\n state?: DialogServiceState | null\n}\n\nconst props = defineProps<GrDialogServiceHostProps>()\n\nconst injected = inject(GRANULARITY_DIALOG_SERVICE_STATE, null)\n\nconst activeState = computed<DialogServiceState | null>(() => props.state ?? injected)\n\nconst visible = computed<DialogRequest[]>(() =>\n activeState.value ? visibleDialogRequests(activeState.value) : [],\n)\n\n// Окна берут состояние отсюда, а не пропом: они его мутируют (очередь, список\n// заявок в полёте), а мутировать проп — значит писать в чужое владение.\nprovide(GRANULARITY_DIALOG_SERVICE_STATE, activeState.value!)\n</script>\n\n<template>\n <GrDialogServiceItem\n v-for=\"request in visible\"\n :key=\"request.id\"\n :request=\"request\"\n />\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrDialogService-CHVR4sOk.js","names":[],"sources":["../../../../src/components/GrDialogService/store.ts","../../../../src/components/GrDialogService/useDialogService.ts","../../../../src/components/GrDialogService/GrDialogServiceItem.vue","../../../../src/components/GrDialogService/GrDialogServiceItem.vue","../../../../src/components/GrDialogService/GrDialogServiceHost.vue","../../../../src/components/GrDialogService/GrDialogServiceHost.vue"],"sourcesContent":["import { reactive } from 'vue'\nimport type { AppContext, Raw } from 'vue'\n\nimport type { GrConfigContext } from '../GrConfigProvider/context'\nimport type { GranularityI18nLike } from '../../internal/granularityI18n'\n\nimport type {\n DialogBaseOptions,\n DialogKind,\n DialogOnConfirm,\n DialogResult,\n} from './types'\n\n/**\n * Внутреннее описание одного запроса диалога в очереди. Создаётся методами\n * сервиса (`confirm`/`alert`/`prompt`/`open`) и потребляется хостом\n * (`GrDialogServiceHost`), который рендерит видимую часть очереди.\n */\nexport interface DialogRequest {\n id: string\n kind: DialogKind\n message: string\n options: DialogBaseOptions\n onConfirm?: DialogOnConfirm<any>\n /** Позиция среди ожидающих: больше — раньше. При равенстве порядок FIFO. */\n priority: number\n /**\n * Заявка создана из `onConfirm` уже открытого диалога и показывается **поверх**\n * него, а не за ним. Без этого вложенный вызов вставал бы в очередь за тем,\n * кто его ждёт: внешнее окно висело бы в загрузке, а промис не резолвился бы\n * никогда.\n */\n nested: boolean\n /**\n * Заявка уже завершена. Флаг живёт на самой заявке, а не на хосте: завершить\n * её могут трое (кнопка в окне, `close()` промиса, `closeAll()`), и общее\n * состояние на хосте означало бы, что второй и третий путь про первый не\n * знают.\n */\n settled: boolean\n /** Резолвит промис вызова детальным результатом `{ action, value }`. */\n resolve: (result: DialogResult<any>) => void\n /**\n * Конфиг и i18n, захваченные в месте вызова сервиса (в `setup`, где вызывающий\n * компонент ещё находится внутри дерева провайдера). Хост монтируется в\n * `document.body` вне дерева, поэтому сам их не увидит — см.\n * `SPEC-GrConfig-resolver.md`.\n *\n * Лежат в запросе, а не в состоянии сервиса: два поддерева с разными\n * провайдерами должны получать каждое свой конфиг.\n *\n * `Raw<…>` обязателен: очередь — `reactive`, а он разворачивает вложенные рефы.\n * Без `markRaw` контекст превратился бы в объект значений, `size.value` стало\n * бы `undefined`, и мост молча перестал бы работать.\n */\n config?: Raw<GrConfigContext> | null\n i18n?: Raw<GranularityI18nLike> | null\n}\n\n/** Контекст, захваченный вызовом `useDialogService()` внутри `setup`. */\nexport interface CapturedDialogContext {\n config: Raw<GrConfigContext> | null\n i18n: Raw<GranularityI18nLike> | null\n}\n\n/**\n * Состояние одного инстанса сервиса: очередь, смонтированный хост и захваченный\n * контекст приложения. Живёт либо в приложении (через\n * `granularityDialogServicePlugin`), либо в ленивом модульном фолбэке для\n * простых SPA — см. `useDialogService.ts`.\n */\nexport interface DialogServiceState {\n /** Очередь заявок. Видимую часть выбирает хост, остальные ждут. */\n queue: DialogRequest[]\n mounted: boolean\n container: HTMLElement | null\n appContext: AppContext | null\n lastCapturedContext: CapturedDialogContext | null\n contextlessWarned: boolean\n /**\n * Заявки, чей `onConfirm` сейчас в полёте, от внешней к самой глубокой.\n * По ним определяется вложенность нового вызова.\n */\n inFlight: DialogRequest[]\n}\n\nexport function createDialogServiceState(): DialogServiceState {\n return {\n queue: reactive<DialogRequest[]>([]),\n mounted: false,\n container: null,\n appContext: null,\n lastCapturedContext: null,\n contextlessWarned: false,\n inFlight: [],\n }\n}\n\nlet counter = 0\n\nexport function makeDialogId(): string {\n counter += 1\n return `gr-dialog-${Date.now()}-${counter}`\n}\n\n/**\n * Видимая часть очереди: голова и непрерывный хвост вложенных заявок за ней.\n * Всё остальное ждёт — обычные вызовы по-прежнему идут по одному, чтобы три\n * алерта из цикла не легли стопкой.\n */\nexport function visibleDialogRequests(state: DialogServiceState): DialogRequest[] {\n const visible: DialogRequest[] = []\n\n for (const request of state.queue) {\n if (visible.length === 0 || request.nested)\n visible.push(request)\n else\n break\n }\n\n return visible\n}\n\n/**\n * Ставит заявку в очередь. Единственное место, где решается её позиция:\n *\n * - вложенная (создана, пока `onConfirm` другой заявки в полёте) встаёт сразу\n * за своим родителем и показывается поверх него;\n * - обычная — перед всеми ожидающими с меньшим приоритетом; показанные окна не\n * трогаются, выдёргивать фокус-ловушку из-под пользователя нельзя.\n */\nexport function enqueueDialogRequest(state: DialogServiceState, request: DialogRequest): void {\n const parent = state.inFlight[state.inFlight.length - 1]\n\n if (parent) {\n request.nested = true\n const parentIndex = state.queue.findIndex(item => item.id === parent.id)\n state.queue.splice(parentIndex + 1, 0, request)\n return\n }\n\n const visibleCount = visibleDialogRequests(state).length\n const insertAt = state.queue.findIndex(\n (item, index) => index >= visibleCount && item.priority < request.priority,\n )\n\n if (insertAt < 0)\n state.queue.push(request)\n else\n state.queue.splice(insertAt, 0, request)\n}\n\n/**\n * Заявка входит в «полёт»: пока её `onConfirm` не вернулся, новые вызовы\n * считаются вложенными в неё.\n */\nexport function startDialogInFlight(state: DialogServiceState, request: DialogRequest): void {\n state.inFlight.push(request)\n}\n\n/** Заявка вышла из полёта (колбэк вернулся или окно размонтировано). */\nexport function finishDialogInFlight(state: DialogServiceState, request: DialogRequest): void {\n const index = state.inFlight.indexOf(request)\n if (index >= 0)\n state.inFlight.splice(index, 1)\n}\n\n/**\n * Единственный способ завершить заявку: идемпотентно резолвит промис и снимает\n * её из очереди.\n *\n * `removeFromQueue: false` нужен окну: оно гасит себя и даёт анимации закрытия\n * доиграть, прежде чем заявка исчезнет из очереди. Корректность от этой\n * отсрочки не зависит: повторное завершение отсекается флагом на заявке.\n */\nexport function settleDialogRequest(\n state: DialogServiceState,\n request: DialogRequest,\n result: DialogResult<any>,\n { removeFromQueue = true }: { removeFromQueue?: boolean } = {},\n): boolean {\n if (request.settled)\n return false\n\n request.settled = true\n request.resolve(result)\n\n if (removeFromQueue)\n removeDialogRequest(state, request)\n\n return true\n}\n\n/** Снимает заявку из очереди, если она ещё там. */\nexport function removeDialogRequest(state: DialogServiceState, request: DialogRequest): void {\n const index = state.queue.findIndex(item => item.id === request.id)\n if (index >= 0)\n state.queue.splice(index, 1)\n}\n","import { createVNode, getCurrentInstance, inject, markRaw, render } from 'vue'\nimport type { App, AppContext, InjectionKey } from 'vue'\n\nimport { ensurePortalRoot } from '../../composables/internal/portalRoot'\nimport GrDialogServiceHost from './GrDialogServiceHost.vue'\nimport { useGrConfig } from '../GrConfigProvider/context'\nimport { resolveGranularityI18n } from '../../internal/granularityI18n'\nimport {\n createDialogServiceState,\n enqueueDialogRequest,\n makeDialogId,\n settleDialogRequest,\n} from './store'\nimport type { CapturedDialogContext, DialogRequest, DialogServiceState } from './store'\nimport type {\n DialogAlertOptions,\n DialogBaseOptions,\n DialogConfirmOptions,\n DialogKind,\n DialogOnConfirm,\n DialogPromise,\n DialogPromptOptions,\n DialogResult,\n DialogService,\n DialogServiceDefaults,\n} from './types'\n\n/**\n * `useDialogService` — императивный сервис диалогов (в духе Element Plus\n * `ElMessageBox`). Позволяет вызывать `confirm`/`alert`/`prompt` из `<script>`\n * или `.ts` без вставки компонента в шаблон.\n *\n * Поверх существующих `GrConfirmDialog`/`GrPromptDialog`: лениво монтирует\n * единый хост (`GrDialogServiceHost`) в `document.body`, наследуя контекст\n * приложения (i18n / тема / `granular-provider`).\n */\n\n/** Ключ provide/inject для app-scoped состояния сервиса (устанавливает плагин ниже). */\nexport const GRANULARITY_DIALOG_SERVICE_STATE: InjectionKey<DialogServiceState>\n = Symbol.for('@feugene/granularity/dialog-service-state')\n\n/**\n * Инстансы, заведённые плагином. Реестр нужен готовому синглтону\n * `dialogService`: он создаётся на импорте модуля, где `inject` не работает\n * вовсе, и сам до app-scoped состояния не дотянется. Ровно один\n * зарегистрированный — ответ однозначен; несколько — выбирать за пользователя\n * нечего, и он получает предупреждение с модульным фолбэком.\n */\nconst registeredStates = new Set<DialogServiceState>()\n\n/**\n * Vue-плагин: даёт каждому приложению собственное состояние сервиса и снимает\n * его хост вместе с приложением. Обязателен для нескольких Vue-приложений на\n * одной странице (микрофронтенды — иначе они делят одну очередь и один хост) и\n * для HMR, где контейнер прошлого приложения иначе остаётся в `document.body`.\n *\n * ```ts\n * app.use(granularityDialogServicePlugin)\n * ```\n */\nexport const granularityDialogServicePlugin = {\n install(app: App) {\n const state = createDialogServiceState()\n\n app.provide(GRANULARITY_DIALOG_SERVICE_STATE, state)\n registeredStates.add(state)\n\n app.onUnmount(() => {\n teardownDialogServiceState(state)\n registeredStates.delete(state)\n })\n },\n}\n\n// Ленивый модульный фолбэк — канонический вариант для простого SPA без плагина.\nlet moduleState: DialogServiceState | null = null\n\nfunction getModuleState(): DialogServiceState {\n if (!moduleState)\n moduleState = createDialogServiceState()\n\n return moduleState\n}\n\nlet ambiguousStateWarned = false\n\nfunction warnAmbiguousState(): void {\n if (ambiguousStateWarned) return\n if (!__GR_DEV__) return\n\n ambiguousStateWarned = true\n console.warn(\n '[granularity] useDialogService: плагин установлен в нескольких приложениях, '\n + 'а сервис запрошен вне `setup` — какое из них имелось в виду, знать неоткуда. '\n + 'Используется модульное состояние. Вызовите `useDialogService()` внутри `setup` нужного приложения.',\n )\n}\n\n/**\n * Состояние текущего вызова: app-scoped (плагин) → единственный\n * зарегистрированный инстанс → модульный фолбэк.\n */\nfunction resolveDialogServiceState(): DialogServiceState {\n // `inject` работает только в `setup` — там ответ точный.\n if (getCurrentInstance()) {\n const provided = inject(GRANULARITY_DIALOG_SERVICE_STATE, null)\n if (provided) return provided\n }\n\n if (registeredStates.size === 1)\n return registeredStates.values().next().value!\n\n if (registeredStates.size > 1)\n warnAmbiguousState()\n\n return getModuleState()\n}\n\nfunction ensureMounted(state: DialogServiceState, appContext?: AppContext | null): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n\n if (appContext) state.appContext = appContext\n if (state.mounted) return\n\n const container = document.createElement('div')\n container.setAttribute('data-gr-dialog-service-host', '')\n container.setAttribute('data-gr-overlay-root', '')\n // Хост уезжает в общий портал оверлеев: он должен лежать в той же ветке, что\n // и остальные слои, иначе правило `inert` пометит его вместе со страницей.\n ;(ensurePortalRoot() ?? document.body).appendChild(container)\n state.container = container\n\n // Состояние отдаём пропом: хост монтируется вне дерева и своим `inject` до\n // app-scoped состояния не дотянулся бы. `markRaw` — чтобы Vue не пытался\n // сделать реактивной саму обёртку: реактивна очередь внутри неё.\n const vnode = createVNode(GrDialogServiceHost, { state: markRaw(state) })\n vnode.appContext = state.appContext ?? null\n render(vnode, container)\n state.mounted = true\n}\n\nfunction warnIfContextless(state: DialogServiceState, context: CapturedDialogContext | null): void {\n if (state.contextlessWarned || context?.config || context?.i18n) return\n if (!__GR_DEV__) return\n\n state.contextlessWarned = true\n console.warn(\n '[granularity] useDialogService: диалог открыт без контекста приложения — '\n + 'ни `GrConfigProvider`, ни адаптер i18n не найдены, будут использованы дефолты. '\n + 'Вызовите `useDialogService()` внутри `setup` хотя бы один раз или передайте `setAppContext`.',\n )\n}\n\n/** Снимает смонтированный хост и сбрасывает захваченный контекст одного инстанса. */\nfunction teardownDialogServiceState(state: DialogServiceState): void {\n if (state.container) {\n render(null, state.container)\n state.container.remove()\n state.container = null\n }\n state.mounted = false\n // Сбрасываем и кэш контекста приложения: без этого он переживает teardown и\n // протекает в следующее приложение (в тестах — в следующий тест), из-за чего\n // диалог может подхватить чужие provides и проверка соврёт.\n state.appContext = null\n state.lastCapturedContext = null\n state.contextlessWarned = false\n state.queue.splice(0, state.queue.length)\n state.inFlight.splice(0, state.inFlight.length)\n}\n\n/**\n * Тестовая/служебная очистка смонтированного хоста. Работает над тем же\n * состоянием, что и сам сервис: app-scoped при вызове из `setup` или при\n * единственном зарегистрированном приложении, иначе — модульным.\n */\nexport function teardownDialogService(): void {\n teardownDialogServiceState(resolveDialogServiceState())\n}\n\nfunction enqueue(\n state: DialogServiceState,\n kind: DialogKind,\n message: string,\n options: DialogBaseOptions,\n onConfirm: DialogOnConfirm<any> | undefined,\n captured: CapturedDialogContext,\n): { promise: Promise<DialogResult<any>>, close: () => void } {\n // Императивный сервис клиент-only: монтирует хост в `document.body`. В SSR\n // выполнять его нельзя — очередь мутировалась бы на сервере и текла между\n // запросами. Явно запрещаем вместо тихого no-op.\n if (typeof window === 'undefined' || typeof document === 'undefined') {\n throw new Error(\n '[granularity] useDialogService is client-only and cannot be called during SSR '\n + '(it mounts a host into document.body). Guard imperative dialog calls behind a client-side check.',\n )\n }\n\n ensureMounted(state, options.appContext ?? state.appContext)\n\n const id = makeDialogId()\n let resolveFn!: (result: DialogResult<any>) => void\n const promise = new Promise<DialogResult<any>>((resolve) => {\n resolveFn = resolve\n })\n\n // Синглтон `dialogService` создаётся на импорте модуля, вне `setup`, и своего\n // контекста не имеет в принципе. Берём последний захваченный — иначе такие\n // вызовы навсегда остаются без i18n и без `GrConfigProvider`.\n const context = captured.config || captured.i18n ? captured : state.lastCapturedContext\n warnIfContextless(state, context)\n\n const request: DialogRequest = {\n id,\n kind,\n message,\n options: { ...options, message },\n onConfirm,\n priority: options.priority ?? 0,\n nested: false,\n settled: false,\n resolve: resolveFn,\n config: context?.config ?? null,\n i18n: context?.i18n ?? null,\n }\n\n enqueueDialogRequest(state, request)\n\n // Закрытие через промис идёт тем же путём, что и кнопка в окне: заявка сама\n // знает, завершена ли она, а окно по её исчезновению свернёт всё, что под неё\n // заведено (в т.ч. оборвёт in-flight `onConfirm` через `abort()`).\n const close = (): void => {\n settleDialogRequest(state, request, { action: 'close' })\n }\n\n return { promise, close }\n}\n\nfunction withClose<T>(promise: Promise<T>, close: () => void): DialogPromise<T> {\n const result = promise as DialogPromise<T>\n result.close = close\n return result\n}\n\nfunction mergeErrorDefaults(\n defaults: DialogServiceDefaults,\n options: DialogBaseOptions,\n): DialogBaseOptions & Pick<DialogConfirmOptions, 'cancelText'> {\n return {\n size: defaults.size,\n confirmText: defaults.confirmText,\n confirmVariant: defaults.confirmVariant,\n confirmTone: defaults.confirmTone,\n cancelText: defaults.cancelText,\n buttonSize: defaults.buttonSize,\n closeOnBackdrop: defaults.closeOnBackdrop,\n closeOnEsc: defaults.closeOnEsc,\n errorParsers: defaults.errorParsers,\n errorTexts: defaults.errorTexts,\n errorMessageKey: defaults.errorMessageKey,\n fieldLabels: defaults.fieldLabels,\n ...options,\n }\n}\n\n/**\n * Создаёт сервис диалогов. Опциональные `defaults` применяются ко всем\n * вызовам (можно переопределить в каждом конкретном вызове).\n *\n * Контекст приложения кэшируется автоматически при первом вызове внутри\n * `setup`. Для вызовов вне компонента используйте `setAppContext`.\n */\nexport function useDialogService(defaults: DialogServiceDefaults = {}): DialogService {\n const state = resolveDialogServiceState()\n\n // Авто-кэш appContext из текущего компонента (если вызвано в setup).\n const instance = getCurrentInstance()\n if (instance?.appContext && !state.appContext) {\n state.appContext = instance.appContext\n }\n\n // Конфиг и i18n захватываем здесь и только здесь: `inject` работает лишь в\n // `setup`, а хост монтируется вне дерева и сам до провайдера не дотянется.\n // Храним в замыкании сервиса, а не в состоянии, — иначе два поддерева с\n // разными провайдерами делили бы конфиг первого.\n // i18n захватываем не «на всякий случай»: дочерний `GrConfirmDialog` сам зовёт\n // `useGranularityTranslations()`, но его `inject` из хоста уходит в\n // `appContext.provides` — то есть видит только установку через `app.use()`.\n // Адаптер, переданный пропом `<GrConfigProvider i18n>`, живёт в дереве, и без\n // захвата диалог откатился бы на встроенные английские строки.\n //\n // `markRaw` — потому что дальше контекст ложится в `reactive`-очередь, а она\n // разворачивает вложенные рефы (см. комментарий в `store.ts`).\n const capturedI18n = instance ? resolveGranularityI18n() : null\n const captured: CapturedDialogContext = {\n config: instance ? markRaw(useGrConfig()) : null,\n i18n: capturedI18n ? markRaw(capturedI18n) : null,\n }\n\n if (captured.config || captured.i18n)\n state.lastCapturedContext = captured\n\n function confirm(message: string, options: DialogConfirmOptions = {}): DialogPromise<boolean> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(state, 'confirm', message, merged, options.onConfirm, captured)\n return withClose(promise.then(r => r.action === 'confirm'), close)\n }\n\n function alert(message: string, options: DialogAlertOptions = {}): DialogPromise<void> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(state, 'alert', message, merged, options.onConfirm, captured)\n return withClose(promise.then(() => undefined), close)\n }\n\n function prompt(message: string, options: DialogPromptOptions = {}): DialogPromise<string | null> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(state, 'prompt', message, merged, options.onConfirm, captured)\n return withClose(\n promise.then(r => (r.action === 'confirm' ? ((r.value as string) ?? '') : null)),\n close,\n )\n }\n\n function open<V = unknown>(\n kind: DialogKind,\n message: string,\n options: DialogBaseOptions = {},\n ): DialogPromise<DialogResult<V>> {\n const merged = mergeErrorDefaults(defaults, options)\n const { promise, close } = enqueue(\n state,\n kind,\n message,\n merged,\n (options as DialogConfirmOptions).onConfirm,\n captured,\n )\n return withClose(promise as Promise<DialogResult<V>>, close)\n }\n\n // Порядок разбора — FIFO, как и порядок показа: `pop()` резолвил промисы\n // задом наперёд, и вызывающий получал результаты в обратном порядке.\n function closeAll(): void {\n for (const request of [...state.queue])\n settleDialogRequest(state, request, { action: 'close' })\n }\n\n function setAppContext(ctx: AppContext | null): void {\n state.appContext = ctx\n }\n\n return { confirm, alert, prompt, open, closeAll, setAppContext }\n}\n\n/** Готовый синглтон-сервис с дефолтными настройками. */\nexport const dialogService: DialogService = {\n confirm: (message, options) => useDialogService().confirm(message, options),\n alert: (message, options) => useDialogService().alert(message, options),\n prompt: (message, options) => useDialogService().prompt(message, options),\n open: (kind, message, options) => useDialogService().open(kind, message, options),\n closeAll: () => useDialogService().closeAll(),\n setAppContext: ctx => useDialogService().setAppContext(ctx),\n}\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceItem — одно окно императивного сервиса. Внутренний компонент,\n * в шаблонах напрямую не используется.\n *\n * Всё состояние живёт здесь, а не на хосте, ровно потому, что окон может быть\n * несколько: вложенный вызов из `onConfirm` показывается поверх родителя, и\n * общий `loading` с общей ошибкой означали бы, что верхнее окно пишет в нижнее.\n */\nimport { computed, inject, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrConfirmDialog from '../GrConfirmDialog/GrConfirmDialog.vue'\nimport GrPromptDialog from '../GrPromptDialog/GrPromptDialog.vue'\nimport {\n coreResponseErrorParsers,\n createResponseErrorClassifier,\n responseErrorParserPresets,\n} from '../GrResponseErrorBanner/parsers'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nimport { GR_CONFIG_KEY, type GrComponentDefaults, type GrConfigContext } from '../GrConfigProvider/context'\nimport { GRANULARITY_I18N_KEY } from '../../i18n/adapter'\nimport { resolveGranularityI18n, useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { finishDialogInFlight, removeDialogRequest, settleDialogRequest, startDialogInFlight } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\nimport type { DialogCloseAction, DialogConfirmContext, DialogConfirmOptions, DialogPromptOptions } from './types'\n\nconst props = defineProps<{\n request: DialogRequest\n}>()\n\n// Состояние инстанса приходит через `provide` хоста, а не пропом: окно его\n// мутирует (очередь, список заявок в полёте), а мутировать проп — значит писать\n// в чужое владение.\nconst state = inject(GRANULARITY_DIALOG_SERVICE_STATE) as DialogServiceState\n\n// ————— Мост конфига и i18n.\n//\n// Окно рендерится через `render(vnode, container)` в `document.body`, то есть вне\n// дерева компонентов. Vue в этом случае берёт `provides` только из `appContext`,\n// куда попадает лишь `app.provide()` — значения от `<GrConfigProvider>` до сюда\n// не доходят. Поэтому вызов сервиса захватывает их в `setup` и кладёт в заявку,\n// а окно раздаёт вниз конфиг **своей** заявки.\nconst EMPTY_DEFAULTS: GrComponentDefaults = Object.freeze({})\n\nprovide<GrConfigContext>(GR_CONFIG_KEY, {\n size: computed(() => props.request.config?.size.value),\n componentDefaults: computed(() => props.request.config?.componentDefaults.value ?? EMPTY_DEFAULTS),\n})\n\n// i18n: тот же приём. Адаптер хоста (из `appContext`) — запасной вариант для\n// вызова без захваченного: без него установка через `app.use()` осталась бы\n// незамеченной.\nconst hostI18n = resolveGranularityI18n()\n\nprovide(GRANULARITY_I18N_KEY, {\n t: (key: string, params?: Record<string, unknown>) => {\n const adapter = props.request.i18n ?? hostI18n\n // Вернуть сам ключ — принятый в пакете сигнал «перевода нет»: потребитель\n // подставит свой fallback-текст (см. `useGranularityTranslations`).\n return adapter?.t(key, params) ?? key\n },\n get locale() {\n return (props.request.i18n ?? hostI18n)?.locale\n },\n})\n\n// Подвал alert-ветки рисуется здесь, поэтому и перевод кнопки достаётся здесь.\n// Адаптер берём тот же, что раздан вниз: собственный `provide` компонента его\n// же `inject` не видит, и `useGranularityTranslations()` без аргумента ушёл бы\n// мимо захваченного вызовом адаптера — в фолбэк из `appContext`.\nconst alertConfirmText = computed(() => {\n const { t } = useGranularityTranslations(props.request.i18n ?? hostI18n)\n return t('gr.dialog.ok', 'OK')\n})\n\nconst open = ref(false)\nconst loading = ref(false)\nconst currentError = shallowRef<ResponseErrorInfo | null>(null)\n/**\n * Ошибки по именам полей. Карта, а не одна строка: `setFieldError(field, …)`\n * обещает адресность, и обещание должно быть правдой ещё до того, как в\n * диалоге появится второе поле.\n */\nconst fieldErrors = ref<Record<string, string>>({})\nconst promptValue = ref(\n props.request.options && 'value' in props.request.options\n ? ((props.request.options as { value?: string }).value ?? '')\n : '',\n)\n\n/** Единственное поле `GrPromptDialog` называется `value`. */\nconst PROMPT_FIELD = 'value'\n\n// Одна запись — её и показываем: адрес не важен, когда адресат один.\nconst promptFieldError = computed<string | null>(() => {\n const own = fieldErrors.value[PROMPT_FIELD]\n if (own != null) return own\n\n const entries = Object.values(fieldErrors.value)\n return entries.length === 1 ? entries[0] : null\n})\n\nconst hasFieldError = computed(() => Object.keys(fieldErrors.value).length > 0)\n\nconst controller = new AbortController()\nlet externalAbortCleanup: (() => void) | null = null\n\nfunction buildClassifier() {\n const opts = props.request.options\n const parsers = typeof opts.errorParsers === 'function'\n ? opts.errorParsers(responseErrorParserPresets)\n : opts.errorParsers ?? coreResponseErrorParsers\n\n return createResponseErrorClassifier({\n parsers,\n texts: opts.errorTexts,\n messageKey: opts.errorMessageKey,\n })\n}\n\nonMounted(() => {\n // Внешний AbortSignal закрывает диалог как `close`.\n const sig = props.request.options.signal\n if (sig) {\n if (sig.aborted) {\n finish('close')\n return\n }\n const onAbort = () => finish('close')\n sig.addEventListener('abort', onAbort)\n externalAbortCleanup = () => sig.removeEventListener('abort', onAbort)\n }\n\n void nextTick(() => {\n open.value = true\n })\n})\n\nonBeforeUnmount(() => {\n // Снимаем всё, что заведено под заявку: подписку на внешний сигнал и свой\n // контроллер. Размонтирование — единственная точка, которую видят все три\n // пути завершения (кнопка, `close()` промиса, `closeAll()`), поэтому обрыв\n // in-flight `onConfirm` живёт именно здесь.\n externalAbortCleanup?.()\n externalAbortCleanup = null\n controller.abort()\n\n finishDialogInFlight(state, props.request)\n})\n\nfunction finish(action: DialogCloseAction, value?: unknown): void {\n // Снятие заявки из очереди откладываем на тик: окно должно успеть закрыться\n // до того, как исчезнет из разметки.\n if (!settleDialogRequest(state, props.request, { action, value }, { removeFromQueue: false }))\n return\n\n open.value = false\n void nextTick(() => removeDialogRequest(state, props.request))\n}\n\nfunction buildContext(value: unknown): DialogConfirmContext<any> & { _errorSet: () => boolean } {\n let errorSet = false\n\n const setError = (message: string | null): void => {\n if (message == null) {\n currentError.value = null\n return\n }\n errorSet = true\n currentError.value = { kind: 'unknown', message, raw: message }\n }\n\n const setFieldError = (field: string, message: string | null): void => {\n const next = { ...fieldErrors.value }\n if (message == null) {\n delete next[field]\n fieldErrors.value = next\n return\n }\n errorSet = true\n next[field] = message\n fieldErrors.value = next\n }\n\n const clearErrors = (): void => {\n currentError.value = null\n fieldErrors.value = {}\n }\n\n const setRawError = async (raw: unknown, meta?: Record<string, unknown>): Promise<ResponseErrorInfo | null> => {\n const info = await buildClassifier()(raw, meta)\n if (info.fieldErrors?.length) {\n const next = { ...fieldErrors.value }\n for (const entry of info.fieldErrors)\n next[entry.field] = entry.messages.join(' ')\n fieldErrors.value = next\n }\n currentError.value = info\n errorSet = true\n return info\n }\n\n return {\n value,\n signal: controller.signal,\n setError,\n setRawError,\n setFieldError,\n clearErrors,\n setLoading: (v: boolean) => { loading.value = v },\n close: (action: DialogCloseAction = 'close') => finish(action),\n _errorSet: () => errorSet,\n }\n}\n\nasync function handleConfirm(): Promise<void> {\n const req = props.request\n const value = req.kind === 'prompt' ? promptValue.value : undefined\n\n if (!req.onConfirm) {\n finish('confirm', value)\n return\n }\n\n currentError.value = null\n fieldErrors.value = {}\n loading.value = true\n\n const ctx = buildContext(value)\n\n // Пока колбэк в полёте, заявка считается родителем для новых вызовов: диалог,\n // открытый изнутри `onConfirm`, показывается поверх, а не встаёт в очередь за\n // тем, кто его ждёт.\n startDialogInFlight(state, req)\n\n try {\n const result = await req.onConfirm(ctx)\n // Сначала проверка «диалог ещё жив», потом запись состояния: за время\n // `await` заявку мог завершить `ctx.close()` или `closeAll()`.\n if (req.settled) return\n loading.value = false\n if (result === false) return // оставить открытым без ошибки\n if (currentError.value || hasFieldError.value || ctx._errorSet()) return // есть ошибка — не закрывать\n finish('confirm', value)\n }\n catch (error) {\n if (req.settled) return\n loading.value = false\n if (!ctx._errorSet()) await ctx.setRawError(error)\n // оставить диалог открытым с показанной ошибкой\n }\n finally {\n finishDialogInFlight(state, req)\n }\n}\n\nfunction handleCancel(): void {\n finish('cancel')\n}\n\nfunction handleModelUpdate(value: boolean): void {\n if (!value) finish('close')\n}\n\n// Опции веток читаются типизированно, без `as any` в шаблоне.\nconst promptOptions = computed<DialogPromptOptions>(() => props.request.options as DialogPromptOptions)\nconst confirmOptions = computed<DialogConfirmOptions>(() => props.request.options as DialogConfirmOptions)\n\n// Прокидываемые в дочерний диалог общие пропы.\nconst sharedProps = computed(() => {\n const o = props.request.options\n return {\n title: o.title,\n size: o.size,\n closeOnBackdrop: o.closeOnBackdrop,\n closeOnEsc: o.closeOnEsc,\n // Подавление «мягкого» закрытия на время `onConfirm` считает сам диалог по\n // `confirmLoading` — здесь только включаем режим, чтобы механизм жил в\n // одном месте, а не дублировался сервисом.\n persistent: true,\n showHeader: o.showHeader,\n showCloseButton: o.showCloseButton,\n headerConfig: o.headerConfig,\n footerConfig: o.footerConfig,\n bodyConfig: o.bodyConfig,\n closeLabel: o.closeLabel,\n buttonSize: o.buttonSize,\n confirmText: o.confirmText,\n confirmVariant: o.confirmVariant,\n confirmTone: o.confirmTone,\n }\n})\n</script>\n\n<template>\n <!-- prompt -->\n <GrPromptDialog\n v-if=\"request.kind === 'prompt'\"\n v-bind=\"sharedProps\"\n v-model:value=\"promptValue\"\n :model-value=\"open\"\n :description=\"request.options.message ?? request.options.description\"\n :label=\"promptOptions.label\"\n :placeholder=\"promptOptions.placeholder\"\n :required=\"promptOptions.required ?? false\"\n :required-error-text=\"promptOptions.requiredErrorText\"\n :cancel-text=\"promptOptions.cancelText\"\n :input-type=\"promptOptions.inputType\"\n :inputmode=\"promptOptions.inputmode\"\n :maxlength=\"promptOptions.maxlength\"\n :show-count=\"promptOptions.showCount\"\n :multiline=\"promptOptions.multiline\"\n :rows=\"promptOptions.rows\"\n :autosize=\"promptOptions.autosize\"\n :rules=\"promptOptions.rules\"\n :error=\"currentError\"\n :field-error=\"promptFieldError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n\n <!-- alert: confirm-диалог без кнопки Cancel -->\n <GrConfirmDialog\n v-else-if=\"request.kind === 'alert'\"\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n >\n <template #footer>\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton\n data-testid=\"gr-alert-confirm\"\n :variant=\"sharedProps.confirmVariant ?? 'primary'\"\n :tone=\"sharedProps.confirmTone ?? 'primary'\"\n :size=\"sharedProps.buttonSize\"\n :loading=\"loading\"\n @click=\"handleConfirm\"\n >\n {{ sharedProps.confirmText ?? alertConfirmText }}\n </GrButton>\n </div>\n </template>\n </GrConfirmDialog>\n\n <!-- confirm -->\n <GrConfirmDialog\n v-else\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :cancel-text=\"confirmOptions.cancelText\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceItem — одно окно императивного сервиса. Внутренний компонент,\n * в шаблонах напрямую не используется.\n *\n * Всё состояние живёт здесь, а не на хосте, ровно потому, что окон может быть\n * несколько: вложенный вызов из `onConfirm` показывается поверх родителя, и\n * общий `loading` с общей ошибкой означали бы, что верхнее окно пишет в нижнее.\n */\nimport { computed, inject, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrConfirmDialog from '../GrConfirmDialog/GrConfirmDialog.vue'\nimport GrPromptDialog from '../GrPromptDialog/GrPromptDialog.vue'\nimport {\n coreResponseErrorParsers,\n createResponseErrorClassifier,\n responseErrorParserPresets,\n} from '../GrResponseErrorBanner/parsers'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nimport { GR_CONFIG_KEY, type GrComponentDefaults, type GrConfigContext } from '../GrConfigProvider/context'\nimport { GRANULARITY_I18N_KEY } from '../../i18n/adapter'\nimport { resolveGranularityI18n, useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { finishDialogInFlight, removeDialogRequest, settleDialogRequest, startDialogInFlight } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\nimport type { DialogCloseAction, DialogConfirmContext, DialogConfirmOptions, DialogPromptOptions } from './types'\n\nconst props = defineProps<{\n request: DialogRequest\n}>()\n\n// Состояние инстанса приходит через `provide` хоста, а не пропом: окно его\n// мутирует (очередь, список заявок в полёте), а мутировать проп — значит писать\n// в чужое владение.\nconst state = inject(GRANULARITY_DIALOG_SERVICE_STATE) as DialogServiceState\n\n// ————— Мост конфига и i18n.\n//\n// Окно рендерится через `render(vnode, container)` в `document.body`, то есть вне\n// дерева компонентов. Vue в этом случае берёт `provides` только из `appContext`,\n// куда попадает лишь `app.provide()` — значения от `<GrConfigProvider>` до сюда\n// не доходят. Поэтому вызов сервиса захватывает их в `setup` и кладёт в заявку,\n// а окно раздаёт вниз конфиг **своей** заявки.\nconst EMPTY_DEFAULTS: GrComponentDefaults = Object.freeze({})\n\nprovide<GrConfigContext>(GR_CONFIG_KEY, {\n size: computed(() => props.request.config?.size.value),\n componentDefaults: computed(() => props.request.config?.componentDefaults.value ?? EMPTY_DEFAULTS),\n})\n\n// i18n: тот же приём. Адаптер хоста (из `appContext`) — запасной вариант для\n// вызова без захваченного: без него установка через `app.use()` осталась бы\n// незамеченной.\nconst hostI18n = resolveGranularityI18n()\n\nprovide(GRANULARITY_I18N_KEY, {\n t: (key: string, params?: Record<string, unknown>) => {\n const adapter = props.request.i18n ?? hostI18n\n // Вернуть сам ключ — принятый в пакете сигнал «перевода нет»: потребитель\n // подставит свой fallback-текст (см. `useGranularityTranslations`).\n return adapter?.t(key, params) ?? key\n },\n get locale() {\n return (props.request.i18n ?? hostI18n)?.locale\n },\n})\n\n// Подвал alert-ветки рисуется здесь, поэтому и перевод кнопки достаётся здесь.\n// Адаптер берём тот же, что раздан вниз: собственный `provide` компонента его\n// же `inject` не видит, и `useGranularityTranslations()` без аргумента ушёл бы\n// мимо захваченного вызовом адаптера — в фолбэк из `appContext`.\nconst alertConfirmText = computed(() => {\n const { t } = useGranularityTranslations(props.request.i18n ?? hostI18n)\n return t('gr.dialog.ok', 'OK')\n})\n\nconst open = ref(false)\nconst loading = ref(false)\nconst currentError = shallowRef<ResponseErrorInfo | null>(null)\n/**\n * Ошибки по именам полей. Карта, а не одна строка: `setFieldError(field, …)`\n * обещает адресность, и обещание должно быть правдой ещё до того, как в\n * диалоге появится второе поле.\n */\nconst fieldErrors = ref<Record<string, string>>({})\nconst promptValue = ref(\n props.request.options && 'value' in props.request.options\n ? ((props.request.options as { value?: string }).value ?? '')\n : '',\n)\n\n/** Единственное поле `GrPromptDialog` называется `value`. */\nconst PROMPT_FIELD = 'value'\n\n// Одна запись — её и показываем: адрес не важен, когда адресат один.\nconst promptFieldError = computed<string | null>(() => {\n const own = fieldErrors.value[PROMPT_FIELD]\n if (own != null) return own\n\n const entries = Object.values(fieldErrors.value)\n return entries.length === 1 ? entries[0] : null\n})\n\nconst hasFieldError = computed(() => Object.keys(fieldErrors.value).length > 0)\n\nconst controller = new AbortController()\nlet externalAbortCleanup: (() => void) | null = null\n\nfunction buildClassifier() {\n const opts = props.request.options\n const parsers = typeof opts.errorParsers === 'function'\n ? opts.errorParsers(responseErrorParserPresets)\n : opts.errorParsers ?? coreResponseErrorParsers\n\n return createResponseErrorClassifier({\n parsers,\n texts: opts.errorTexts,\n messageKey: opts.errorMessageKey,\n })\n}\n\nonMounted(() => {\n // Внешний AbortSignal закрывает диалог как `close`.\n const sig = props.request.options.signal\n if (sig) {\n if (sig.aborted) {\n finish('close')\n return\n }\n const onAbort = () => finish('close')\n sig.addEventListener('abort', onAbort)\n externalAbortCleanup = () => sig.removeEventListener('abort', onAbort)\n }\n\n void nextTick(() => {\n open.value = true\n })\n})\n\nonBeforeUnmount(() => {\n // Снимаем всё, что заведено под заявку: подписку на внешний сигнал и свой\n // контроллер. Размонтирование — единственная точка, которую видят все три\n // пути завершения (кнопка, `close()` промиса, `closeAll()`), поэтому обрыв\n // in-flight `onConfirm` живёт именно здесь.\n externalAbortCleanup?.()\n externalAbortCleanup = null\n controller.abort()\n\n finishDialogInFlight(state, props.request)\n})\n\nfunction finish(action: DialogCloseAction, value?: unknown): void {\n // Снятие заявки из очереди откладываем на тик: окно должно успеть закрыться\n // до того, как исчезнет из разметки.\n if (!settleDialogRequest(state, props.request, { action, value }, { removeFromQueue: false }))\n return\n\n open.value = false\n void nextTick(() => removeDialogRequest(state, props.request))\n}\n\nfunction buildContext(value: unknown): DialogConfirmContext<any> & { _errorSet: () => boolean } {\n let errorSet = false\n\n const setError = (message: string | null): void => {\n if (message == null) {\n currentError.value = null\n return\n }\n errorSet = true\n currentError.value = { kind: 'unknown', message, raw: message }\n }\n\n const setFieldError = (field: string, message: string | null): void => {\n const next = { ...fieldErrors.value }\n if (message == null) {\n delete next[field]\n fieldErrors.value = next\n return\n }\n errorSet = true\n next[field] = message\n fieldErrors.value = next\n }\n\n const clearErrors = (): void => {\n currentError.value = null\n fieldErrors.value = {}\n }\n\n const setRawError = async (raw: unknown, meta?: Record<string, unknown>): Promise<ResponseErrorInfo | null> => {\n const info = await buildClassifier()(raw, meta)\n if (info.fieldErrors?.length) {\n const next = { ...fieldErrors.value }\n for (const entry of info.fieldErrors)\n next[entry.field] = entry.messages.join(' ')\n fieldErrors.value = next\n }\n currentError.value = info\n errorSet = true\n return info\n }\n\n return {\n value,\n signal: controller.signal,\n setError,\n setRawError,\n setFieldError,\n clearErrors,\n setLoading: (v: boolean) => { loading.value = v },\n close: (action: DialogCloseAction = 'close') => finish(action),\n _errorSet: () => errorSet,\n }\n}\n\nasync function handleConfirm(): Promise<void> {\n const req = props.request\n const value = req.kind === 'prompt' ? promptValue.value : undefined\n\n if (!req.onConfirm) {\n finish('confirm', value)\n return\n }\n\n currentError.value = null\n fieldErrors.value = {}\n loading.value = true\n\n const ctx = buildContext(value)\n\n // Пока колбэк в полёте, заявка считается родителем для новых вызовов: диалог,\n // открытый изнутри `onConfirm`, показывается поверх, а не встаёт в очередь за\n // тем, кто его ждёт.\n startDialogInFlight(state, req)\n\n try {\n const result = await req.onConfirm(ctx)\n // Сначала проверка «диалог ещё жив», потом запись состояния: за время\n // `await` заявку мог завершить `ctx.close()` или `closeAll()`.\n if (req.settled) return\n loading.value = false\n if (result === false) return // оставить открытым без ошибки\n if (currentError.value || hasFieldError.value || ctx._errorSet()) return // есть ошибка — не закрывать\n finish('confirm', value)\n }\n catch (error) {\n if (req.settled) return\n loading.value = false\n if (!ctx._errorSet()) await ctx.setRawError(error)\n // оставить диалог открытым с показанной ошибкой\n }\n finally {\n finishDialogInFlight(state, req)\n }\n}\n\nfunction handleCancel(): void {\n finish('cancel')\n}\n\nfunction handleModelUpdate(value: boolean): void {\n if (!value) finish('close')\n}\n\n// Опции веток читаются типизированно, без `as any` в шаблоне.\nconst promptOptions = computed<DialogPromptOptions>(() => props.request.options as DialogPromptOptions)\nconst confirmOptions = computed<DialogConfirmOptions>(() => props.request.options as DialogConfirmOptions)\n\n// Прокидываемые в дочерний диалог общие пропы.\nconst sharedProps = computed(() => {\n const o = props.request.options\n return {\n title: o.title,\n size: o.size,\n closeOnBackdrop: o.closeOnBackdrop,\n closeOnEsc: o.closeOnEsc,\n // Подавление «мягкого» закрытия на время `onConfirm` считает сам диалог по\n // `confirmLoading` — здесь только включаем режим, чтобы механизм жил в\n // одном месте, а не дублировался сервисом.\n persistent: true,\n showHeader: o.showHeader,\n showCloseButton: o.showCloseButton,\n headerConfig: o.headerConfig,\n footerConfig: o.footerConfig,\n bodyConfig: o.bodyConfig,\n closeLabel: o.closeLabel,\n buttonSize: o.buttonSize,\n confirmText: o.confirmText,\n confirmVariant: o.confirmVariant,\n confirmTone: o.confirmTone,\n }\n})\n</script>\n\n<template>\n <!-- prompt -->\n <GrPromptDialog\n v-if=\"request.kind === 'prompt'\"\n v-bind=\"sharedProps\"\n v-model:value=\"promptValue\"\n :model-value=\"open\"\n :description=\"request.options.message ?? request.options.description\"\n :label=\"promptOptions.label\"\n :placeholder=\"promptOptions.placeholder\"\n :required=\"promptOptions.required ?? false\"\n :required-error-text=\"promptOptions.requiredErrorText\"\n :cancel-text=\"promptOptions.cancelText\"\n :input-type=\"promptOptions.inputType\"\n :inputmode=\"promptOptions.inputmode\"\n :maxlength=\"promptOptions.maxlength\"\n :show-count=\"promptOptions.showCount\"\n :multiline=\"promptOptions.multiline\"\n :rows=\"promptOptions.rows\"\n :autosize=\"promptOptions.autosize\"\n :rules=\"promptOptions.rules\"\n :error=\"currentError\"\n :field-error=\"promptFieldError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n\n <!-- alert: confirm-диалог без кнопки Cancel -->\n <GrConfirmDialog\n v-else-if=\"request.kind === 'alert'\"\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n >\n <template #footer>\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton\n data-testid=\"gr-alert-confirm\"\n :variant=\"sharedProps.confirmVariant ?? 'primary'\"\n :tone=\"sharedProps.confirmTone ?? 'primary'\"\n :size=\"sharedProps.buttonSize\"\n :loading=\"loading\"\n @click=\"handleConfirm\"\n >\n {{ sharedProps.confirmText ?? alertConfirmText }}\n </GrButton>\n </div>\n </template>\n </GrConfirmDialog>\n\n <!-- confirm -->\n <GrConfirmDialog\n v-else\n :model-value=\"open\"\n v-bind=\"sharedProps\"\n :description=\"request.options.message ?? request.options.description\"\n :cancel-text=\"confirmOptions.cancelText\"\n :error=\"currentError\"\n :confirm-loading=\"loading\"\n :close-on-confirm=\"false\"\n @update:model-value=\"handleModelUpdate\"\n @confirm=\"handleConfirm\"\n @cancel=\"handleCancel\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceHost — внутренний компонент-оркестратор императивного\n * `useDialogService`. Не предназначен для прямого использования в шаблонах.\n *\n * Держит раскладку очереди и ничего больше: состояние каждой заявки — загрузка,\n * ошибки, введённое значение, мост конфига и i18n — живёт в\n * `GrDialogServiceItem`, потому что окон может быть несколько.\n *\n * Видимы голова очереди и её вложенные окна (`store.visibleDialogRequests`):\n * обычные вызовы по-прежнему показываются по одному, а диалог, открытый из\n * `onConfirm` другого диалога, ложится поверх родителя — иначе он ждал бы в\n * очереди того, кто ждёт его.\n */\nimport { computed, inject, provide } from 'vue'\n\nimport GrDialogServiceItem from './GrDialogServiceItem.vue'\nimport { visibleDialogRequests } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\n\nexport interface GrDialogServiceHostProps {\n /**\n * Состояние инстанса сервиса. Приходит пропом при программном монтировании:\n * хост живёт вне дерева и своим `inject` до app-scoped состояния не дотянулся\n * бы. `inject` остаётся для случая, когда хост поставили в шаблон руками.\n */\n state?: DialogServiceState | null\n}\n\nconst props = defineProps<GrDialogServiceHostProps>()\n\nconst injected = inject(GRANULARITY_DIALOG_SERVICE_STATE, null)\n\nconst activeState = computed<DialogServiceState | null>(() => props.state ?? injected)\n\nconst visible = computed<DialogRequest[]>(() =>\n activeState.value ? visibleDialogRequests(activeState.value) : [],\n)\n\n// Окна берут состояние отсюда, а не пропом: они его мутируют (очередь, список\n// заявок в полёте), а мутировать проп — значит писать в чужое владение.\nprovide(GRANULARITY_DIALOG_SERVICE_STATE, activeState.value!)\n</script>\n\n<template>\n <GrDialogServiceItem\n v-for=\"request in visible\"\n :key=\"request.id\"\n :request=\"request\"\n />\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrDialogServiceHost — внутренний компонент-оркестратор императивного\n * `useDialogService`. Не предназначен для прямого использования в шаблонах.\n *\n * Держит раскладку очереди и ничего больше: состояние каждой заявки — загрузка,\n * ошибки, введённое значение, мост конфига и i18n — живёт в\n * `GrDialogServiceItem`, потому что окон может быть несколько.\n *\n * Видимы голова очереди и её вложенные окна (`store.visibleDialogRequests`):\n * обычные вызовы по-прежнему показываются по одному, а диалог, открытый из\n * `onConfirm` другого диалога, ложится поверх родителя — иначе он ждал бы в\n * очереди того, кто ждёт его.\n */\nimport { computed, inject, provide } from 'vue'\n\nimport GrDialogServiceItem from './GrDialogServiceItem.vue'\nimport { visibleDialogRequests } from './store'\nimport type { DialogRequest, DialogServiceState } from './store'\nimport { GRANULARITY_DIALOG_SERVICE_STATE } from './useDialogService'\n\nexport interface GrDialogServiceHostProps {\n /**\n * Состояние инстанса сервиса. Приходит пропом при программном монтировании:\n * хост живёт вне дерева и своим `inject` до app-scoped состояния не дотянулся\n * бы. `inject` остаётся для случая, когда хост поставили в шаблон руками.\n */\n state?: DialogServiceState | null\n}\n\nconst props = defineProps<GrDialogServiceHostProps>()\n\nconst injected = inject(GRANULARITY_DIALOG_SERVICE_STATE, null)\n\nconst activeState = computed<DialogServiceState | null>(() => props.state ?? injected)\n\nconst visible = computed<DialogRequest[]>(() =>\n activeState.value ? visibleDialogRequests(activeState.value) : [],\n)\n\n// Окна берут состояние отсюда, а не пропом: они его мутируют (очередь, список\n// заявок в полёте), а мутировать проп — значит писать в чужое владение.\nprovide(GRANULARITY_DIALOG_SERVICE_STATE, activeState.value!)\n</script>\n\n<template>\n <GrDialogServiceItem\n v-for=\"request in visible\"\n :key=\"request.id\"\n :request=\"request\"\n />\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as useDialogService, i as teardownDialogService, n as dialogService, r as granularityDialogServicePlugin, t as GrDialogServiceHost_default } from "./chunks/GrDialogService-
|
|
1
|
+
import { a as useDialogService, i as teardownDialogService, n as dialogService, r as granularityDialogServicePlugin, t as GrDialogServiceHost_default } from "./chunks/GrDialogService-CHVR4sOk.js";
|
|
2
2
|
import { t as grDialogServiceConfig } from "../../chunks/config-CjXjhphs.js";
|
|
3
3
|
export { GrDialogServiceHost_default as GrDialogServiceHost, dialogService, grDialogServiceConfig, granularityDialogServicePlugin, teardownDialogService, useDialogService };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
2
2
|
import { t as GR_FORM_KEY } from "../../../chunks/context-Bfrgsluh.js";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as rulesRequired, c as toRuleArray, i as rulesForTrigger, n as getByPath, o as runFieldRules, s as setByPath, t as createGrFormMessageResolver } from "../../../chunks/validation-5HhpanPP.js";
|
|
4
4
|
import { computed, createElementBlock, defineComponent, onBeforeUnmount, openBlock, provide, ref, renderSlot, watch, withModifiers } from "vue";
|
|
5
5
|
//#region src/components/GrForm/modelSnapshot.ts
|
|
6
6
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrForm-B_Qq_13k.js","names":[],"sources":["../../../../src/components/GrForm/modelSnapshot.ts","../../../../src/components/GrForm/GrForm.vue","../../../../src/components/GrForm/GrForm.vue"],"sourcesContent":["/**\n * Снимок модели формы: клон для `resetFields` и отпечаток для `isDirty`.\n *\n * Чистый модуль без Vue: обе операции читают «сквозь» reactive-прокси и\n * тестируются без монтирования.\n *\n * Почему не `JSON.parse(JSON.stringify(...))` и не `structuredClone`: первый\n * теряет `File` (у файла нет ни перечислимых собственных свойств, ни `toJSON`,\n * поэтому в снимок попадал `{}`), второй падает на прокси с `DataCloneError`.\n */\n\n/** `File` и прочие `Blob` — двоичные значения, для которых клон бессмыслен. */\nfunction isBinary(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob\n}\n\nfunction cloneValue(value: unknown): unknown {\n if (typeof value === 'function' || typeof value === 'symbol') return undefined\n if (value === null || typeof value !== 'object') return value\n\n // По ссылке: содержимое файла клонировать нечем, а идентичность как раз и\n // нужна — `resetFields` обязан вернуть тот самый файл, а не его подобие.\n if (isBinary(value)) return value\n\n if (Array.isArray(value)) {\n // Как у `JSON.stringify`: не-сериализуемый элемент массива становится `null`,\n // иначе сброс сдвинул бы индексы.\n return value.map((item) => {\n const cloned = cloneValue(item)\n return cloned === undefined ? null : cloned\n })\n }\n\n // `Date` и прочие носители `toJSON` ведут себя ровно как раньше.\n const toJSON = (value as { toJSON?: unknown }).toJSON\n if (typeof toJSON === 'function') return cloneValue((value as { toJSON: () => unknown }).toJSON())\n\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) {\n const cloned = cloneValue(item)\n if (cloned !== undefined) out[key] = cloned\n }\n return out\n}\n\n/** Клон значения модели: JSON-семантика плюс сохранённые по ссылке файлы. */\nexport function cloneModelValue<T>(value: T): T {\n return value === undefined ? value : (cloneValue(value) as T)\n}\n\n/**\n * Отпечаток файла для сравнения. Тождество здесь не годится: `resetFields`\n * кладёт в модель клон снимка, и сравнение по ссылке считало бы форму грязной\n * сразу после сброса. Тройки «имя, размер, время» достаточно, чтобы отличить\n * один выбранный файл от другого.\n */\nfunction binaryFingerprint(value: Blob): string {\n if (typeof File !== 'undefined' && value instanceof File)\n return `gr-file:${value.name}:${value.size}:${value.lastModified}`\n\n return `gr-blob:${value.size}:${value.type}`\n}\n\n/**\n * Строка, по которой сравниваются модель и снимок. Без неё любой `File`\n * сериализовался в `{}`, и «выбрал документ» было неотличимо от «не трогал».\n */\nexport function modelFingerprint(value: unknown): string {\n return JSON.stringify(value, (_key, item) => isBinary(item) ? binaryFingerprint(item) : item) ?? ''\n}\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrForm-CAlJg-wb.js","names":[],"sources":["../../../../src/components/GrForm/modelSnapshot.ts","../../../../src/components/GrForm/GrForm.vue","../../../../src/components/GrForm/GrForm.vue"],"sourcesContent":["/**\n * Снимок модели формы: клон для `resetFields` и отпечаток для `isDirty`.\n *\n * Чистый модуль без Vue: обе операции читают «сквозь» reactive-прокси и\n * тестируются без монтирования.\n *\n * Почему не `JSON.parse(JSON.stringify(...))` и не `structuredClone`: первый\n * теряет `File` (у файла нет ни перечислимых собственных свойств, ни `toJSON`,\n * поэтому в снимок попадал `{}`), второй падает на прокси с `DataCloneError`.\n */\n\n/** `File` и прочие `Blob` — двоичные значения, для которых клон бессмыслен. */\nfunction isBinary(value: unknown): value is Blob {\n return typeof Blob !== 'undefined' && value instanceof Blob\n}\n\nfunction cloneValue(value: unknown): unknown {\n if (typeof value === 'function' || typeof value === 'symbol') return undefined\n if (value === null || typeof value !== 'object') return value\n\n // По ссылке: содержимое файла клонировать нечем, а идентичность как раз и\n // нужна — `resetFields` обязан вернуть тот самый файл, а не его подобие.\n if (isBinary(value)) return value\n\n if (Array.isArray(value)) {\n // Как у `JSON.stringify`: не-сериализуемый элемент массива становится `null`,\n // иначе сброс сдвинул бы индексы.\n return value.map((item) => {\n const cloned = cloneValue(item)\n return cloned === undefined ? null : cloned\n })\n }\n\n // `Date` и прочие носители `toJSON` ведут себя ровно как раньше.\n const toJSON = (value as { toJSON?: unknown }).toJSON\n if (typeof toJSON === 'function') return cloneValue((value as { toJSON: () => unknown }).toJSON())\n\n const out: Record<string, unknown> = {}\n for (const [key, item] of Object.entries(value)) {\n const cloned = cloneValue(item)\n if (cloned !== undefined) out[key] = cloned\n }\n return out\n}\n\n/** Клон значения модели: JSON-семантика плюс сохранённые по ссылке файлы. */\nexport function cloneModelValue<T>(value: T): T {\n return value === undefined ? value : (cloneValue(value) as T)\n}\n\n/**\n * Отпечаток файла для сравнения. Тождество здесь не годится: `resetFields`\n * кладёт в модель клон снимка, и сравнение по ссылке считало бы форму грязной\n * сразу после сброса. Тройки «имя, размер, время» достаточно, чтобы отличить\n * один выбранный файл от другого.\n */\nfunction binaryFingerprint(value: Blob): string {\n if (typeof File !== 'undefined' && value instanceof File)\n return `gr-file:${value.name}:${value.size}:${value.lastModified}`\n\n return `gr-blob:${value.size}:${value.type}`\n}\n\n/**\n * Строка, по которой сравниваются модель и снимок. Без неё любой `File`\n * сериализовался в `{}`, и «выбрал документ» было неотличимо от «не трогал».\n */\nexport function modelFingerprint(value: unknown): string {\n return JSON.stringify(value, (_key, item) => isBinary(item) ? binaryFingerprint(item) : item) ?? ''\n}\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n","<script setup lang=\"ts\" generic=\"TModel extends object = Record<string, unknown>\">\nimport { computed, onBeforeUnmount, provide, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { GR_FORM_KEY } from './context'\nimport { cloneModelValue, modelFingerprint } from './modelSnapshot'\nimport {\n getByPath,\n createGrFormMessageResolver,\n rulesForTrigger,\n rulesRequired,\n runFieldRules,\n setByPath,\n toRuleArray,\n type GrFormRule,\n type GrFormRules,\n type GrFormTrigger,\n} from './validation'\n\nexport type {\n GrFormFileRule,\n GrFormRule,\n GrFormRules,\n GrFormTrigger,\n GrFormValidatorResult,\n} from './validation'\n\n/**\n * Публичный GR-примитив «Form» — оркестрация валидации над `GrFormField`.\n *\n * Модель формы (`v-model`-объект приложения) + декларативные `rules` по имени поля.\n * Форма прогоняет правила, раскладывает ошибки по контексту, скроллит к первой\n * ошибке и эмитит `submit` только при валидной форме. Императивный API — через\n * template ref (`validate` / `validateField` / `clearValidate` / `resetFields` /\n * `scrollToField`).\n */\nexport interface GrFormProps<TModel extends object = Record<string, unknown>> {\n /**\n * Реактивный объект данных формы. Поля адресуются по `name` (`GrFormField`),\n * в т.ч. dot-path.\n *\n * Тип дженерик, а не `Record<string, unknown>`: моделью бывает объект чужой\n * библиотеки (`useForm` Inertia, стор), у которого рядом с полями лежат\n * собственные методы. Под словарь он не подходит, и потребителю пришлось бы\n * приводить его через `as unknown as` в каждой форме.\n */\n model: TModel\n /** Правила валидации по имени поля: `{ email: [{ required: true, type: 'email' }] }`. */\n rules?: GrFormRules\n /** Валидировать поле при потере фокуса. */\n validateOnBlur?: boolean\n /** Валидировать поле при каждом изменении значения. */\n validateOnChange?: boolean\n /** Скроллить к первому невалидному полю после `validate()`. */\n scrollToError?: boolean\n scrollBehavior?: ScrollBehavior\n /**\n * Выключить форму целиком — типично на время отправки. Доезжает до контролов\n * через контекст поля, поэтому обходить их по одному не нужно.\n */\n disabled?: boolean\n}\n\nexport interface GrFormEmits<TModel extends object = Record<string, unknown>> {\n /** Форма прошла валидацию по submit. Отдаёт `model`. */\n (e: 'submit', model: TModel): void\n /** Результат валидации одного поля. */\n (e: 'validate', name: string, valid: boolean, message: string | undefined): void\n /**\n * Submit не прошёл валидацию. Без этого события «форма невалидна» и «ничего\n * не произошло» выглядят для потребителя одинаково.\n */\n (e: 'invalid', errors: Record<string, string>): void\n}\n\nconst props = withDefaults(\n defineProps<GrFormProps<TModel>>(),\n {\n rules: undefined,\n validateOnBlur: true,\n validateOnChange: false,\n scrollToError: true,\n scrollBehavior: 'smooth',\n disabled: false,\n },\n)\n\nconst emit = defineEmits<GrFormEmits<TModel>>()\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Модель наружу дженерик, а адресация полей внутри работает по строковому пути.\n * Приведение безопасно: форма читает и пишет только по именам, объявленным\n * полями (`GrFormField name`), а не по всему объекту.\n */\nconst modelRecord = computed(() => props.model as Record<string, unknown>)\n\n// ————— Ошибки и реестр полей.\nconst errors = ref<Record<string, string | undefined>>({})\nconst fieldRegistry = new Map<string, () => HTMLElement | null>()\n\n/**\n * Поля, объявившие себя обязательными собственным пропом `required` — без\n * правила в `rules`. До этого они рисовали звёздочку, но submit пропускал их\n * пустыми: обязательность и валидация жили в разных местах.\n */\nconst selfRequiredFields = ref<Set<string>>(new Set())\n\n/** Поля с идущей асинхронной проверкой. */\nconst validatingSet = ref<Set<string>>(new Set())\nconst validatingFields = computed(() => validatingSet.value)\n\nfunction getRules(name: string): GrFormRule[] {\n return toRuleArray(props.rules?.[name])\n}\n\nfunction getValue(name: string): unknown {\n return getByPath(modelRecord.value, name)\n}\n\nconst requiredFields = computed(() => {\n const set = new Set<string>(selfRequiredFields.value)\n for (const name of Object.keys(props.rules ?? {})) {\n if (rulesRequired(getRules(name))) set.add(name)\n }\n return set\n})\n\n/** Все поля, которые форма обязана проверить: с правилами и обязательные без них. */\nconst validatedNames = computed(() => [\n ...new Set([...Object.keys(props.rules ?? {}), ...selfRequiredFields.value]),\n])\n\n/**\n * Правила поля с учётом неявной обязательности: `<GrFormField required>` без\n * записи в `rules` проверяется тем же `required`-правилом и тем же сообщением,\n * что и явное.\n */\nconst IMPLICIT_REQUIRED_RULE: GrFormRule = { required: true }\n\nfunction effectiveRules(name: string): GrFormRule[] {\n const rules = getRules(name)\n if (rules.length) return rules\n return selfRequiredFields.value.has(name) ? [IMPLICIT_REQUIRED_RULE] : []\n}\n\n// Дефолтные i18n-сообщения (перекрываются `rule.message`) — общие с любым\n// потребителем движка правил, не только с формой.\nconst resolveMessage = createGrFormMessageResolver(t)\n\n// ————— Валидация.\n// Счётчик поколений на имя поля: перекрывающиеся асинхронные прогоны пишут\n// результат в порядке РАЗРЕШЕНИЯ промисов, и без него медленный ответ про\n// старое значение затирал бы свежий (приём — `searchSeq` в GrAutocomplete).\nconst validationSeq = new Map<string, number>()\n// Последний стартовавший прогон поля: вытесненному нечего сказать о текущем\n// значении, и он отдаёт вердикт того, кто его вытеснил, — иначе `validate()`\n// собрал бы «валидно» из карты ошибок, куда новый прогон ещё не записал.\nconst validationRuns = new Map<string, { seq: number, promise: Promise<boolean> }>()\n\nasync function validateField(name: string, trigger?: GrFormTrigger): Promise<boolean> {\n if (trigger === 'blur' && !props.validateOnBlur) return !errors.value[name]\n\n const rules = rulesForTrigger(effectiveRules(name), trigger)\n if (!rules.length) return !errors.value[name]\n\n const seq = (validationSeq.get(name) ?? 0) + 1\n validationSeq.set(name, seq)\n\n const run = runValidation(name, seq, rules)\n validationRuns.set(name, { seq, promise: run })\n return run\n}\n\nasync function runValidation(name: string, seq: number, rules: GrFormRule[]): Promise<boolean> {\n // Признак «идёт проверка» нужен именно асинхронным правилам: без него поле\n // молчит до ответа сервера, и пользователь не знает, что что-то происходит.\n validatingSet.value = new Set(validatingSet.value).add(name)\n try {\n const message = await runFieldRules(getValue(name), rules, modelRecord.value, resolveMessage)\n\n // Устаревший прогон: его результат — про прежнее значение, не применять.\n if (validationSeq.get(name) !== seq) {\n const current = validationRuns.get(name)\n // Цепочка конечна: каждый следующий прогон новее предыдущего. Своего\n // прогона в реестре нет только после `clearValidate` — там проверять\n // нечего, форма уже чиста.\n return current && current.seq !== seq ? current.promise : !errors.value[name]\n }\n\n errors.value = { ...errors.value, [name]: message }\n emit('validate', name, !message, message)\n return !message\n }\n finally {\n if (validationSeq.get(name) === seq) {\n const next = new Set(validatingSet.value)\n next.delete(name)\n validatingSet.value = next\n }\n }\n}\n\nasync function validate(): Promise<boolean> {\n const results = await Promise.all(validatedNames.value.map(name => validateField(name)))\n const valid = results.every(Boolean)\n if (!valid && props.scrollToError) scrollToFirstError()\n return valid\n}\n\n/**\n * Отменить летящие проверки: их ответ относится к состоянию до сброса, и\n * записать его в очищенную форму значило бы вернуть пользователю ошибку,\n * которую он только что убрал. Заодно снимаем «проверяем» — ждать сервер,\n * чтобы погасить индикатор на сброшенном поле, не за чем.\n */\nfunction invalidateValidation(names?: string[]): void {\n const list = names ?? [...validationSeq.keys()]\n if (!list.length) return\n\n const nextValidating = new Set(validatingSet.value)\n for (const name of list) {\n validationSeq.set(name, (validationSeq.get(name) ?? 0) + 1)\n validationRuns.delete(name)\n nextValidating.delete(name)\n }\n validatingSet.value = nextValidating\n}\n\nfunction clearValidate(names?: string | string[]): void {\n if (!names) {\n invalidateValidation()\n errors.value = {}\n return\n }\n const list = Array.isArray(names) ? names : [names]\n invalidateValidation(list)\n const next = { ...errors.value }\n for (const name of list) delete next[name]\n errors.value = next\n}\n\nconst initialSnapshot = ref<Record<string, unknown>>(cloneModelValue(modelRecord.value))\n\n/**\n * Переснять «исходное» состояние. Нужен формам редактирования: модель там\n * наполняется после ответа сервера, а снимок в `setup` фиксировал пустой\n * объект — и `resetFields()` возвращал не «как было при загрузке», а пустоту.\n */\nfunction setSnapshot(model?: Record<string, unknown>): void {\n initialSnapshot.value = cloneModelValue((model ?? props.model) as Record<string, unknown>)\n}\n\n/**\n * Удаление ключа модели. Вынесено в функцию не ради красоты: модель — проп, и\n * прямое `delete props.model[key]` линтер справедливо считает мутацией пропа.\n * Мутируем мы при этом по контракту — `model` у формы и есть общий объект\n * данных приложения, ради которого форма и существует.\n */\nfunction removeKey(model: Record<string, unknown>, key: string): void {\n delete model[key]\n}\n\nfunction resetFields(names?: string | string[]): void {\n const snapshot = initialSnapshot.value\n const model = modelRecord.value\n const list = names ? (Array.isArray(names) ? names : [names]) : undefined\n\n // Ключи объединяем: поле, появившееся после снимка, надо **удалить**, а не\n // выставить в `undefined` — иначе «сброс» оставляет за собой мусор.\n const keys = list ?? [...new Set([...Object.keys(model), ...Object.keys(snapshot)])]\n\n // Метод не удаляем, даже если его нет в снимке. Снимок строится клоном, а клон\n // отбрасывает функции — то есть для внешней модели (`useForm` Inertia, стор)\n // «нет в снимке» означает «это её метод», и сброс снёс бы саму модель.\n for (const key of keys) {\n if (key in snapshot) setByPath(model, key, cloneModelValue(snapshot[key]))\n else if (typeof model[key] !== 'function') removeKey(model, key)\n }\n\n clearValidate(list)\n}\n\n/** Модель отличается от снимка. Файлы сравниваются отпечатком, а не ссылкой. */\nconst isDirty = computed(() => modelFingerprint(modelRecord.value) !== modelFingerprint(initialSnapshot.value))\n\n/**\n * Известных ошибок нет. Это не «валидация прошла»: до первого `validate()`\n * ошибок нет просто потому, что никто не проверял.\n */\nconst isValid = computed(() => Object.values(errors.value).every(message => !message))\n\n// ————— Scroll-to-error.\nfunction scrollToField(name: string): void {\n const el = fieldRegistry.get(name)?.()\n if (!el) return\n el.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n const focusable = el.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')\n focusable?.focus?.()\n}\n\nfunction scrollToFirstError(): void {\n const elements = [...fieldRegistry.entries()]\n .filter(([name]) => errors.value[name])\n .map(([, getEl]) => getEl())\n .filter((el): el is HTMLElement => Boolean(el))\n\n // Первое по порядку в DOM — «самое верхнее» невалидное поле на экране.\n elements.sort((a, b) =>\n (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1,\n )\n\n const first = elements[0]\n if (!first) return\n first.scrollIntoView({ behavior: props.scrollBehavior, block: 'center' })\n first.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]:not([tabindex=\"-1\"])')?.focus?.()\n}\n\n// ————— «Умный» change: чистим ошибку по мере исправления; валидируем при validateOnChange.\nlet stopWatchers: Array<() => void> = []\nwatch(\n // Ключ — строка имён, а не массив: идентичность массива меняется на каждый\n // пересчёт геттера, и все пер-полевые watcher'ы пересоздавались впустую.\n () => validatedNames.value.join(' '),\n () => {\n stopWatchers.forEach(stop => stop())\n stopWatchers = validatedNames.value.map(name =>\n watch(\n () => getValue(name),\n () => {\n if (props.validateOnChange) void validateField(name, 'change')\n else if (errors.value[name]) void validateField(name) // ре-валидация, чтобы снять ошибку\n },\n { deep: true },\n ),\n )\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => stopWatchers.forEach(stop => stop()))\n\n// ————— Контекст для `GrFormField`.\nprovide(GR_FORM_KEY, {\n errors,\n requiredFields,\n validatingFields,\n disabled: computed(() => props.disabled),\n hasField: (name: string) => effectiveRules(name).length > 0,\n registerField: (name, getEl, registration) => {\n fieldRegistry.set(name, getEl)\n\n const stopRequired = registration?.required\n ? watch(\n registration.required,\n (required) => {\n const next = new Set(selfRequiredFields.value)\n if (required) next.add(name)\n else next.delete(name)\n selfRequiredFields.value = next\n },\n { immediate: true },\n )\n : undefined\n\n return () => {\n fieldRegistry.delete(name)\n // Иначе счётчики поколений копятся на динамических именах\n // (`items.417.email`) до размонтирования формы.\n validationSeq.delete(name)\n validationRuns.delete(name)\n stopRequired?.()\n const next = new Set(selfRequiredFields.value)\n next.delete(name)\n selfRequiredFields.value = next\n }\n },\n validateField,\n})\n\nfunction onSubmit(): void {\n void validate().then((valid) => {\n if (valid) {\n emit('submit', props.model)\n return\n }\n\n const failed: Record<string, string> = {}\n for (const [name, message] of Object.entries(errors.value)) {\n if (message) failed[name] = message\n }\n emit('invalid', failed)\n })\n}\n\ndefineExpose({\n validate,\n validateField,\n clearValidate,\n resetFields,\n scrollToField,\n setSnapshot,\n isDirty,\n isValid,\n validatingFields,\n})\n</script>\n\n<template>\n <form data-gr-form novalidate @submit.prevent=\"onSubmit\">\n <slot\n :validate=\"validate\"\n :errors=\"errors\"\n :is-dirty=\"isDirty\"\n :is-valid=\"isValid\"\n :validating-fields=\"validatingFields\"\n :reset-fields=\"resetFields\"\n :set-snapshot=\"setSnapshot\"\n />\n </form>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as useGrFormContext } from "../../chunks/context-Bfrgsluh.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as GrForm_default } from "./chunks/GrForm-
|
|
2
|
+
import { n as getByPath, o as runFieldRules, r as isEmpty, s as setByPath, t as createGrFormMessageResolver } from "../../chunks/validation-5HhpanPP.js";
|
|
3
|
+
import { t as GrForm_default } from "./chunks/GrForm-CAlJg-wb.js";
|
|
4
4
|
import { t as grFormConfig } from "../../chunks/config-Droqn55j.js";
|
|
5
|
-
export { GrForm_default as GrForm, GrForm_default as default, createGrFormMessageResolver, grFormConfig, runFieldRules, useGrFormContext };
|
|
5
|
+
export { GrForm_default as GrForm, GrForm_default as default, createGrFormMessageResolver, getByPath, grFormConfig, isEmpty, runFieldRules, setByPath, useGrFormContext };
|
|
@@ -6,7 +6,7 @@ import { t as GrDialog_default } from "../../GrDialog/chunks/GrDialog-5Y5K87ss.j
|
|
|
6
6
|
import { t as GrResponseErrorBanner_default } from "../../GrResponseErrorBanner/chunks/GrResponseErrorBanner-BhP-xw2P.js";
|
|
7
7
|
import { t as GrFormField_default } from "../../GrFormField/chunks/GrFormField-DSylU7IU.js";
|
|
8
8
|
import { t as GrTextarea_default } from "../../GrTextarea/chunks/GrTextarea-CNnkcIG1.js";
|
|
9
|
-
import {
|
|
9
|
+
import { i as rulesForTrigger, o as runFieldRules, t as createGrFormMessageResolver } from "../../../chunks/validation-5HhpanPP.js";
|
|
10
10
|
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, openBlock, ref, renderSlot, toDisplayString, unref, watch, withCtx, withKeys } from "vue";
|
|
11
11
|
//#region src/components/GrPromptDialog/GrPromptDialog.vue?vue&type=script&setup=true&lang.ts
|
|
12
12
|
var _hoisted_1 = { class: "grid gap-4" };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPromptDialog-DjF-5oD9.js","names":[],"sources":["../../../../src/components/GrPromptDialog/GrPromptDialog.vue","../../../../src/components/GrPromptDialog/GrPromptDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /** Тип однострочного поля. В многострочном режиме не применяется. */\n inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n /**\n * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen) return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules) return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required) return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value) return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length) return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun) return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun) validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline) textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue) return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /** Тип однострочного поля. В многострочном режиме не применяется. */\n inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n /**\n * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen) return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules) return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required) return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value) return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length) return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun) return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun) validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline) textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue) return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrPromptDialog-BGuMUzlv.js","names":[],"sources":["../../../../src/components/GrPromptDialog/GrPromptDialog.vue","../../../../src/components/GrPromptDialog/GrPromptDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /** Тип однострочного поля. В многострочном режиме не применяется. */\n inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n /**\n * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen) return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules) return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required) return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value) return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length) return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun) return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun) validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline) textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue) return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /** Тип однострочного поля. В многострочном режиме не применяется. */\n inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n /**\n * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen) return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules) return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required) return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value) return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length) return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun) return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun) validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline) textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue) return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrPromptDialog_default } from "./chunks/GrPromptDialog-
|
|
1
|
+
import { t as GrPromptDialog_default } from "./chunks/GrPromptDialog-BGuMUzlv.js";
|
|
2
2
|
import { t as grPromptDialogConfig } from "../../chunks/config-C0Ea9ePP.js";
|
|
3
3
|
export { GrPromptDialog_default as GrPromptDialog, GrPromptDialog_default as default, grPromptDialogConfig };
|
package/dist/index.js
CHANGED
|
@@ -98,9 +98,9 @@ import "./components/GrDialog/index.js";
|
|
|
98
98
|
import { n as useGrFormContext } from "./chunks/context-Bfrgsluh.js";
|
|
99
99
|
import { t as GrFormField_default } from "./components/GrFormField/chunks/GrFormField-DSylU7IU.js";
|
|
100
100
|
import { t as GrTextarea_default } from "./components/GrTextarea/chunks/GrTextarea-CNnkcIG1.js";
|
|
101
|
-
import {
|
|
102
|
-
import { t as GrPromptDialog_default } from "./components/GrPromptDialog/chunks/GrPromptDialog-
|
|
103
|
-
import { a as useDialogService, i as teardownDialogService, n as dialogService, r as granularityDialogServicePlugin, t as GrDialogServiceHost_default } from "./components/GrDialogService/chunks/GrDialogService-
|
|
101
|
+
import { n as getByPath, o as runFieldRules, r as isEmpty, s as setByPath, t as createGrFormMessageResolver } from "./chunks/validation-5HhpanPP.js";
|
|
102
|
+
import { t as GrPromptDialog_default } from "./components/GrPromptDialog/chunks/GrPromptDialog-BGuMUzlv.js";
|
|
103
|
+
import { a as useDialogService, i as teardownDialogService, n as dialogService, r as granularityDialogServicePlugin, t as GrDialogServiceHost_default } from "./components/GrDialogService/chunks/GrDialogService-CHVR4sOk.js";
|
|
104
104
|
import { t as grDialogServiceConfig } from "./chunks/config-CjXjhphs.js";
|
|
105
105
|
import { n as grDividerSafelist, t as grDividerConfig } from "./chunks/config-CY0_6cMi.js";
|
|
106
106
|
import { t as GrDivider_default } from "./components/GrDivider/chunks/GrDivider-CAhm9jbE.js";
|
|
@@ -118,7 +118,7 @@ import { n as grFilePreviewSafelist, t as grFilePreviewConfig } from "./chunks/c
|
|
|
118
118
|
import { t as GrProgressBar_default } from "./components/GrProgressBar/chunks/GrProgressBar-hszUOC8f.js";
|
|
119
119
|
import { n as GrUploadAbortError, r as GrUploadHttpError, t as GrFileUpload_default } from "./components/GrFileUpload/chunks/GrFileUpload-wNXzHb5N.js";
|
|
120
120
|
import { n as grFileUploadSafelist, t as grFileUploadConfig } from "./chunks/config-CN970ZcJ.js";
|
|
121
|
-
import { t as GrForm_default } from "./components/GrForm/chunks/GrForm-
|
|
121
|
+
import { t as GrForm_default } from "./components/GrForm/chunks/GrForm-CAlJg-wb.js";
|
|
122
122
|
import { t as grFormConfig } from "./chunks/config-Droqn55j.js";
|
|
123
123
|
import { n as grFormFieldSafelist, t as grFormFieldConfig } from "./chunks/config-D6W1rnlp.js";
|
|
124
124
|
import "./components/GrFormField/index.js";
|
|
@@ -209,4 +209,4 @@ import { t as GrTreeSelect_default } from "./components/GrTreeSelect/chunks/GrTr
|
|
|
209
209
|
import "./composables/useGrComponentConfig.js";
|
|
210
210
|
import "./composables/useGranularityTranslations.js";
|
|
211
211
|
import { GRANULARITY_THEME_STATE, granularityThemePlugin, initThemeEarly, useTheme } from "./composables/useTheme.js";
|
|
212
|
-
export { FileValidationError, GRANULARITY_THEME_STATE, GRANULARITY_TOAST_STATE, GR_KBD_TOKENS, GR_TONES, GrAlert_default as GrAlert, GrAutocomplete_default as GrAutocomplete, GrAvatar_default as GrAvatar, GrAvatarGroup_default as GrAvatarGroup, GrBadge_default as GrBadge, GrBadgeWrap_default as GrBadgeWrap, GrBottomNav_default as GrBottomNav, GrBreadcrumbs_default as GrBreadcrumbs, GrButton_default as GrButton, GrButtonGroup_default as GrButtonGroup, GrCard_default as GrCard, GrCheckbox_default as GrCheckbox, GrCheckboxGroup_default as GrCheckboxGroup, GrChip_default as GrChip, GrChipGroup_default as GrChipGroup, GrCodeBlock_default as GrCodeBlock, GrCollapse_default as GrCollapse, GrCollapseItem_default as GrCollapseItem, GrColorPicker_default as GrColorPicker, GrCommandPalette_default as GrCommandPalette, GrConfigProvider_default as GrConfigProvider, GrConfirmDialog_default as GrConfirmDialog, GrContextMenu_default as GrContextMenu, GrDataTable_default as GrDataTable, GrDelta_default as GrDelta, GrDescriptionList_default as GrDescriptionList, GrDialog_default as GrDialog, GrDialogCloseButton_default as GrDialogCloseButton, GrDialogFooter_default as GrDialogFooter, GrDialogHeader_default as GrDialogHeader, GrDialogServiceHost_default as GrDialogServiceHost, GrDivider_default as GrDivider, GrDrawer_default as GrDrawer, GrDropdown_default as GrDropdown, GrDropdownMenu_default as GrDropdownMenu, GrDropdownMenuColumn_default as GrDropdownMenuColumn, GrDropdownMenuColumns_default as GrDropdownMenuColumns, GrDropdownMenuDivider_default as GrDropdownMenuDivider, GrDropdownMenuGroup_default as GrDropdownMenuGroup, GrDropdownMenuHeader_default as GrDropdownMenuHeader, GrDropdownMenuItem_default as GrDropdownMenuItem, GrDropdownMenuList_default as GrDropdownMenuList, GrEmptyState_default as GrEmptyState, GrFilePreview_default as GrFilePreview, GrFileUpload_default as GrFileUpload, GrForm_default as GrForm, GrFormErrorBanner_default as GrFormErrorBanner, GrFormField_default as GrFormField, GrFormFile_default as GrFormFile, GrFormSection_default as GrFormSection, GrIcon_default as GrIcon, GrImageViewer_default as GrImageViewer, GrInput_default as GrInput, GrInputTag_default as GrInputTag, GrJsonViewer_default as GrJsonViewer, GrKbd_default as GrKbd, GrLink_default as GrLink, GrList_default as GrList, GrListItem_default as GrListItem, GrLoading_default as GrLoading, GrModal_default as GrModal, GrNavbar_default as GrNavbar, GrNumberInput_default as GrNumberInput, GrPagination_default as GrPagination, GrPopover_default as GrPopover, GrProgressBar_default as GrProgressBar, GrProgressCircle_default as GrProgressCircle, GrPromptDialog_default as GrPromptDialog, GrRadio_default as GrRadio, GrRadioGroup_default as GrRadioGroup, GrRating_default as GrRating, GrResponseErrorBanner_default as GrResponseErrorBanner, GrSegmented_default as GrSegmented, GrSelect_default as GrSelect, GrSidebar_default as GrSidebar, GrSidebarGroup_default as GrSidebarGroup, GrSidebarItem_default as GrSidebarItem, GrSkeleton_default as GrSkeleton, GrSlider_default as GrSlider, GrSortableList_default as GrSortableList, GrSplitter_default as GrSplitter, GrStatistic_default as GrStatistic, GrSteps_default as GrSteps, GrSwitch_default as GrSwitch, GrTabPanel_default as GrTabPanel, GrTabPanels_default as GrTabPanels, GrTable_default as GrTable, GrTabs_default as GrTabs, GrTextarea_default as GrTextarea, GrTimeline_default as GrTimeline, GrTimelineItem_default as GrTimelineItem, GrToaster_default as GrToaster, GrTooltip_default as GrTooltip, GrTree_default as GrTree, GrTreeSelect_default as GrTreeSelect, GrUploadAbortError, GrUploadErrorBanner_default as GrUploadErrorBanner, GrUploadHttpError, GrValue_default as GrValue, coreResponseErrorParsers, createGrFormMessageResolver, createLoading, createResponseErrorClassifier, deltaDirection, deltaTone, dialogService, extendDefaultParsers, fileKindOf, findKbdToken, formatBadgeValue, formatStatisticValue, grAlertConfig, grAutocompleteConfig, grAutocompleteSafelist, grAvatarConfig, grAvatarSafelist, grBadgeConfig, grBadgeSafelist, grBadgeWrapConfig, grBadgeWrapSafelist, grBottomNavConfig, grBottomNavSafelist, grBreadcrumbsConfig, grBreadcrumbsSafelist, grButtonConfig, grButtonGroupConfig, grButtonSafelist, grCardConfig, grCardSafelist, grCheckboxConfig, grCheckboxGroupConfig, grCheckboxSafelist, grChipConfig, grChipGroupConfig, grChipGroupSafelist, grChipSafelist, grCodeBlockConfig, grCodeBlockSafelist, grCollapseConfig, grCollapseSafelist, grColorPickerConfig, grColorPickerSafelist, grCommandPaletteConfig, grCommandPaletteSafelist, grConfigProviderConfig, grConfirmDialogConfig, grContextMenuConfig, grDataTableConfig, grDataTableSafelist, grDeltaConfig, grDeltaSafelist, grDescriptionListConfig, grDescriptionListSafelist, grDialogSafelist, grDialogServiceConfig, grDividerConfig, grDividerSafelist, grDrawerConfig, grDrawerSafelist, grDropdownConfig, grDropdownMenuConfig, grDropdownMenuSafelist, grDropdownSafelist, grEmptyStateConfig, grEmptyStateSafelist, grFilePreviewConfig, grFilePreviewSafelist, grFileUploadConfig, grFileUploadSafelist, grFormConfig, grFormFieldConfig, grFormFieldSafelist, grFormFileConfig, grFormFileSafelist, grFormSectionConfig, grFormSectionSafelist, grIconConfig, grIconSafelist, grImageViewerConfig, grImageViewerSafelist, grInputConfig, grInputSafelist, grInputTagConfig, grInputTagSafelist, grJsonViewerConfig, grJsonViewerSafelist, grKbdConfig, grKbdSafelist, grLinkConfig, grLinkSafelist, grListConfig, grListSafelist, grLoadingConfig, grLoadingSafelist, grModalConfig, grModalSafelist, grNavbarConfig, grNavbarSafelist, grNumberInputConfig, grNumberInputSafelist, grPaginationConfig, grPaginationSafelist, grPopoverConfig, grPopoverSafelist, grProgressBarConfig, grProgressBarSafelist, grProgressCircleConfig, grProgressCircleSafelist, grPromptDialogConfig, grRadioConfig, grRadioGroupConfig, grRadioSafelist, grRatingConfig, grRatingSafelist, grSegmentedConfig, grSegmentedSafelist, grSelectConfig, grSelectSafelist, grSidebarConfig, grSidebarSafelist, grSkeletonConfig, grSliderConfig, grSliderSafelist, grSortableListConfig, grSortableListSafelist, grSplitterConfig, grSplitterSafelist, grStatisticConfig, grStatisticSafelist, grStepsConfig, grStepsSafelist, grSwitchConfig, grSwitchSafelist, grTabPanelsConfig, grTableConfig, grTableSafelist, grTabsConfig, grTabsSafelist, grTextareaConfig, grTextareaSafelist, grTimelineConfig, grTimelineSafelist, grToasterConfig, grToasterSafelist, grTooltipConfig, grTooltipSafelist, grTreeConfig, grTreeSafelist, grTreeSelectConfig, grTreeSelectSafelist, grValueConfig, grValueSafelist, granularityDialogServicePlugin, granularityThemePlugin, granularityToastPlugin, initThemeEarly, initialsFrom, isPreviewableKind, normalizeError, resolveBreadcrumbsFit, resolveBreadcrumbsLayout, resolveGrConfig, runFieldRules, sizeFromPointer, teardownDialogService, titleWhenTruncated, useAnnouncer, useComboboxNavigation, useDialogService, useDismissible, useDragGesture, useDragSort, useFloating, useFocusTrap, useGrButtonGroup, useGrComponentDefaults, useGrComponentProp, useGrComponentSize, useGrConfig, useGrFormContext, useGrFormControl, useGrFormFieldContext, useGrThemeAttrs, useGranularityTranslations, useOverlayLayer, usePortalTarget, useResponseError, useRovingFocus, useTheme, useToast, useVirtualList, vAutofocus, vAutosize, vClickOutside, vDropzone, vHotkey, vLoading };
|
|
212
|
+
export { FileValidationError, GRANULARITY_THEME_STATE, GRANULARITY_TOAST_STATE, GR_KBD_TOKENS, GR_TONES, GrAlert_default as GrAlert, GrAutocomplete_default as GrAutocomplete, GrAvatar_default as GrAvatar, GrAvatarGroup_default as GrAvatarGroup, GrBadge_default as GrBadge, GrBadgeWrap_default as GrBadgeWrap, GrBottomNav_default as GrBottomNav, GrBreadcrumbs_default as GrBreadcrumbs, GrButton_default as GrButton, GrButtonGroup_default as GrButtonGroup, GrCard_default as GrCard, GrCheckbox_default as GrCheckbox, GrCheckboxGroup_default as GrCheckboxGroup, GrChip_default as GrChip, GrChipGroup_default as GrChipGroup, GrCodeBlock_default as GrCodeBlock, GrCollapse_default as GrCollapse, GrCollapseItem_default as GrCollapseItem, GrColorPicker_default as GrColorPicker, GrCommandPalette_default as GrCommandPalette, GrConfigProvider_default as GrConfigProvider, GrConfirmDialog_default as GrConfirmDialog, GrContextMenu_default as GrContextMenu, GrDataTable_default as GrDataTable, GrDelta_default as GrDelta, GrDescriptionList_default as GrDescriptionList, GrDialog_default as GrDialog, GrDialogCloseButton_default as GrDialogCloseButton, GrDialogFooter_default as GrDialogFooter, GrDialogHeader_default as GrDialogHeader, GrDialogServiceHost_default as GrDialogServiceHost, GrDivider_default as GrDivider, GrDrawer_default as GrDrawer, GrDropdown_default as GrDropdown, GrDropdownMenu_default as GrDropdownMenu, GrDropdownMenuColumn_default as GrDropdownMenuColumn, GrDropdownMenuColumns_default as GrDropdownMenuColumns, GrDropdownMenuDivider_default as GrDropdownMenuDivider, GrDropdownMenuGroup_default as GrDropdownMenuGroup, GrDropdownMenuHeader_default as GrDropdownMenuHeader, GrDropdownMenuItem_default as GrDropdownMenuItem, GrDropdownMenuList_default as GrDropdownMenuList, GrEmptyState_default as GrEmptyState, GrFilePreview_default as GrFilePreview, GrFileUpload_default as GrFileUpload, GrForm_default as GrForm, GrFormErrorBanner_default as GrFormErrorBanner, GrFormField_default as GrFormField, GrFormFile_default as GrFormFile, GrFormSection_default as GrFormSection, GrIcon_default as GrIcon, GrImageViewer_default as GrImageViewer, GrInput_default as GrInput, GrInputTag_default as GrInputTag, GrJsonViewer_default as GrJsonViewer, GrKbd_default as GrKbd, GrLink_default as GrLink, GrList_default as GrList, GrListItem_default as GrListItem, GrLoading_default as GrLoading, GrModal_default as GrModal, GrNavbar_default as GrNavbar, GrNumberInput_default as GrNumberInput, GrPagination_default as GrPagination, GrPopover_default as GrPopover, GrProgressBar_default as GrProgressBar, GrProgressCircle_default as GrProgressCircle, GrPromptDialog_default as GrPromptDialog, GrRadio_default as GrRadio, GrRadioGroup_default as GrRadioGroup, GrRating_default as GrRating, GrResponseErrorBanner_default as GrResponseErrorBanner, GrSegmented_default as GrSegmented, GrSelect_default as GrSelect, GrSidebar_default as GrSidebar, GrSidebarGroup_default as GrSidebarGroup, GrSidebarItem_default as GrSidebarItem, GrSkeleton_default as GrSkeleton, GrSlider_default as GrSlider, GrSortableList_default as GrSortableList, GrSplitter_default as GrSplitter, GrStatistic_default as GrStatistic, GrSteps_default as GrSteps, GrSwitch_default as GrSwitch, GrTabPanel_default as GrTabPanel, GrTabPanels_default as GrTabPanels, GrTable_default as GrTable, GrTabs_default as GrTabs, GrTextarea_default as GrTextarea, GrTimeline_default as GrTimeline, GrTimelineItem_default as GrTimelineItem, GrToaster_default as GrToaster, GrTooltip_default as GrTooltip, GrTree_default as GrTree, GrTreeSelect_default as GrTreeSelect, GrUploadAbortError, GrUploadErrorBanner_default as GrUploadErrorBanner, GrUploadHttpError, GrValue_default as GrValue, coreResponseErrorParsers, createGrFormMessageResolver, createLoading, createResponseErrorClassifier, deltaDirection, deltaTone, dialogService, extendDefaultParsers, fileKindOf, findKbdToken, formatBadgeValue, formatStatisticValue, getByPath, grAlertConfig, grAutocompleteConfig, grAutocompleteSafelist, grAvatarConfig, grAvatarSafelist, grBadgeConfig, grBadgeSafelist, grBadgeWrapConfig, grBadgeWrapSafelist, grBottomNavConfig, grBottomNavSafelist, grBreadcrumbsConfig, grBreadcrumbsSafelist, grButtonConfig, grButtonGroupConfig, grButtonSafelist, grCardConfig, grCardSafelist, grCheckboxConfig, grCheckboxGroupConfig, grCheckboxSafelist, grChipConfig, grChipGroupConfig, grChipGroupSafelist, grChipSafelist, grCodeBlockConfig, grCodeBlockSafelist, grCollapseConfig, grCollapseSafelist, grColorPickerConfig, grColorPickerSafelist, grCommandPaletteConfig, grCommandPaletteSafelist, grConfigProviderConfig, grConfirmDialogConfig, grContextMenuConfig, grDataTableConfig, grDataTableSafelist, grDeltaConfig, grDeltaSafelist, grDescriptionListConfig, grDescriptionListSafelist, grDialogSafelist, grDialogServiceConfig, grDividerConfig, grDividerSafelist, grDrawerConfig, grDrawerSafelist, grDropdownConfig, grDropdownMenuConfig, grDropdownMenuSafelist, grDropdownSafelist, grEmptyStateConfig, grEmptyStateSafelist, grFilePreviewConfig, grFilePreviewSafelist, grFileUploadConfig, grFileUploadSafelist, grFormConfig, grFormFieldConfig, grFormFieldSafelist, grFormFileConfig, grFormFileSafelist, grFormSectionConfig, grFormSectionSafelist, grIconConfig, grIconSafelist, grImageViewerConfig, grImageViewerSafelist, grInputConfig, grInputSafelist, grInputTagConfig, grInputTagSafelist, grJsonViewerConfig, grJsonViewerSafelist, grKbdConfig, grKbdSafelist, grLinkConfig, grLinkSafelist, grListConfig, grListSafelist, grLoadingConfig, grLoadingSafelist, grModalConfig, grModalSafelist, grNavbarConfig, grNavbarSafelist, grNumberInputConfig, grNumberInputSafelist, grPaginationConfig, grPaginationSafelist, grPopoverConfig, grPopoverSafelist, grProgressBarConfig, grProgressBarSafelist, grProgressCircleConfig, grProgressCircleSafelist, grPromptDialogConfig, grRadioConfig, grRadioGroupConfig, grRadioSafelist, grRatingConfig, grRatingSafelist, grSegmentedConfig, grSegmentedSafelist, grSelectConfig, grSelectSafelist, grSidebarConfig, grSidebarSafelist, grSkeletonConfig, grSliderConfig, grSliderSafelist, grSortableListConfig, grSortableListSafelist, grSplitterConfig, grSplitterSafelist, grStatisticConfig, grStatisticSafelist, grStepsConfig, grStepsSafelist, grSwitchConfig, grSwitchSafelist, grTabPanelsConfig, grTableConfig, grTableSafelist, grTabsConfig, grTabsSafelist, grTextareaConfig, grTextareaSafelist, grTimelineConfig, grTimelineSafelist, grToasterConfig, grToasterSafelist, grTooltipConfig, grTooltipSafelist, grTreeConfig, grTreeSafelist, grTreeSelectConfig, grTreeSelectSafelist, grValueConfig, grValueSafelist, granularityDialogServicePlugin, granularityThemePlugin, granularityToastPlugin, initThemeEarly, initialsFrom, isEmpty, isPreviewableKind, normalizeError, resolveBreadcrumbsFit, resolveBreadcrumbsLayout, resolveGrConfig, runFieldRules, setByPath, sizeFromPointer, teardownDialogService, titleWhenTruncated, useAnnouncer, useComboboxNavigation, useDialogService, useDismissible, useDragGesture, useDragSort, useFloating, useFocusTrap, useGrButtonGroup, useGrComponentDefaults, useGrComponentProp, useGrComponentSize, useGrConfig, useGrFormContext, useGrFormControl, useGrFormFieldContext, useGrThemeAttrs, useGranularityTranslations, useOverlayLayer, usePortalTarget, useResponseError, useRovingFocus, useTheme, useToast, useVirtualList, vAutofocus, vAutosize, vClickOutside, vDropzone, vHotkey, vLoading };
|
|
@@ -5,6 +5,6 @@ export { default as GrForm } from './GrForm.vue';
|
|
|
5
5
|
export { grFormConfig } from './config';
|
|
6
6
|
export { type GrFormContext, useGrFormContext } from './context';
|
|
7
7
|
export type { GrFormFileRule, GrFormProps, GrFormRule, GrFormRules, GrFormTrigger, GrFormValidatorResult, } from './GrForm.vue';
|
|
8
|
-
export { createGrFormMessageResolver, type GrFormMessageResolver, type GrFormMessageTranslate, type GrFormRuleFailure, runFieldRules, } from './validation';
|
|
8
|
+
export { createGrFormMessageResolver, getByPath, type GrFormMessageResolver, type GrFormMessageTranslate, type GrFormRuleFailure, isEmpty, runFieldRules, setByPath, } from './validation';
|
|
9
9
|
export type { GrFormEmits } from './GrForm.vue';
|
|
10
10
|
export type GrFormInstance = ComponentExposed<typeof GrFormComponent>;
|
package/dist/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
|
3
3
|
"name": "@feugene/granularity",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"framework": "vue",
|
|
6
6
|
"js-types-syntax": "typescript",
|
|
7
7
|
"description-markup": "markdown",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feugene/granularity",
|
|
3
3
|
"description": "Granularity design system package with Vue 3 components, consumed via the `@feugene/unocss-preset-granular` preset.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Evgeniy Fureev",
|