diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 78323214..af35697b 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -88,10 +88,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" - | `visible: false` | Скрыть (синоним: `hidden: true`) | | `enabled: false` | Сделать недоступным (синоним: `disabled: true`) | | `readOnly: true` | Только чтение | -| `on: [...]` | События с автоименованием обработчиков | -| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` | +| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы | -### Допустимые имена событий (`on`) +### Допустимые имена событий (`events`) Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано. @@ -440,7 +439,7 @@ PictureField, привязанный к булеву/числу, рисует и "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, "elements": [ { "group": "horizontal", "name": "ГруппаФайл", "children": [ - { "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] }, + { "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } }, { "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" } ]}, { "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" }, @@ -500,8 +499,8 @@ PictureField, привязанный к булеву/числу, рисует и "title": "Просмотр данных", "elements": [ { "group": "horizontal", "name": "Фильтр", "children": [ - { "input": "Период", "path": "Период", "on": ["OnChange"] }, - { "input": "Организация", "path": "Организация", "on": ["OnChange"] } + { "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } }, + { "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } } ]}, { "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [ { "input": "Дата", "path": "Данные.Дата" }, @@ -525,7 +524,6 @@ PictureField, привязанный к булеву/числу, рисует и ## Автогенерация - **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически -- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении` - **Namespace**: все 17 namespace-деклараций - **ID**: последовательная нумерация, AutoCommandBar = id="-1" - **Unknown keys**: выводится предупреждение о нераспознанных ключах diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index fa62765b..64efc660 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.31 — Compile 1C managed form from JSON or object metadata +# form-compile v1.32 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -1803,30 +1803,57 @@ $script:knownEvents = @{ } $script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") +# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. +# Основной формат: $el.events = { Событие: ИмяОбработчика } (null/"" → авто-имя по конвенции). +# Legacy (принимается ради совместимости): $el.on (массив) + $el.handlers (переопределение имён). +function Get-EventPairs { + param($el, [string]$elementName) + $pairs = New-Object System.Collections.ArrayList + if ($el.events) { + foreach ($p in $el.events.PSObject.Properties) { + $h = "$($p.Value)" + if ([string]::IsNullOrEmpty($h)) { $h = Get-HandlerName -elementName $elementName -eventName $p.Name } + [void]$pairs.Add([pscustomobject]@{ name = $p.Name; handler = $h }) + } + } elseif ($el.on) { + foreach ($evt in $el.on) { + $evtName = "$evt" + $h = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } else { Get-HandlerName -elementName $elementName -eventName $evtName } + [void]$pairs.Add([pscustomobject]@{ name = $evtName; handler = $h }) + } + } + return $pairs +} + +# Проверить, подключено ли событие к элементу (в любом из форматов). +function Test-ElementEvent { + param($el, [string]$eventName) + if ($el.events) { + foreach ($p in $el.events.PSObject.Properties) { if ($p.Name -eq $eventName) { return $true } } + } + if ($el.on -contains $eventName) { return $true } + return $false +} + function Emit-Events { param($el, [string]$elementName, [string]$indent, [string]$typeKey) - if (-not $el.on) { return } + $pairs = Get-EventPairs -el $el -elementName $elementName + if ($pairs.Count -eq 0) { return } # Validate event names if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { $allowed = $script:knownEvents[$typeKey] - foreach ($evt in $el.on) { - if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") { - Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + foreach ($pr in $pairs) { + if ($allowed.Count -gt 0 -and $allowed -notcontains "$($pr.name)") { + Write-Host "[WARN] Unknown event '$($pr.name)' for $typeKey '$elementName'. Known: $($allowed -join ', ')" } } } X "$indent" - foreach ($evt in $el.on) { - $evtName = "$evt" - $handler = if ($el.handlers -and $el.handlers.$evtName) { - "$($el.handlers.$evtName)" - } else { - Get-HandlerName -elementName $elementName -eventName $evtName - } - X "$indent`t$handler" + foreach ($pr in $pairs) { + X "$indent`t$($pr.handler)" } X "$indent" } @@ -1932,8 +1959,8 @@ function Emit-Element { "name"=1;"path"=1;"title"=1 # visibility & state "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1;"userVisible"=1 - # events - "on"=1;"handlers"=1 + # events ("events" — основной формат; on/handlers — legacy, принимаются ради совместимости) + "events"=1;"on"=1;"handlers"=1 # layout "titleLocation"=1;"representation"=1;"width"=1;"height"=1 "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 @@ -2218,7 +2245,7 @@ function Emit-Input { if ($el.multiLine -eq $true) { X "$innertrue" } if ($el.passwordMode -eq $true) { X "$innertrue" } if ($el.choiceButton -eq $false) { X "$innerfalse" } - elseif ($el.choiceButton -eq $true -and ($el.on -contains 'StartChoice')) { X "$innertrue" } + elseif ($el.choiceButton -eq $true -and (Test-ElementEvent $el 'StartChoice')) { X "$innertrue" } if ($el.clearButton -eq $true) { X "$innertrue" } if ($el.spinButton -eq $true) { X "$innertrue" } if ($el.dropListButton -eq $true) { X "$innertrue" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index df7adf63..47c34a45 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.31 — Compile 1C managed form from JSON or object metadata +# form-compile v1.32 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1356,7 +1356,7 @@ KNOWN_KEYS = { "radioButtonType", "choiceList", "columnsCount", "checkBoxType", "editMode", "name", "path", "title", "visible", "hidden", "enabled", "disabled", "readOnly", "userVisible", - "on", "handlers", + "events", "on", "handlers", "titleLocation", "representation", "width", "height", "horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight", "maxWidth", "maxHeight", @@ -1533,26 +1533,53 @@ def get_element_name(el, type_key): return str(el.get(type_key, '')) +# Собрать упорядоченный список событий элемента (имя, обработчик) из DSL. +# Основной формат: el['events'] = { Событие: ИмяОбработчика } (None/"" → авто-имя по конвенции). +# Legacy (принимается ради совместимости): el['on'] (массив) + el['handlers'] (переопределение имён). +def get_event_pairs(el, element_name): + pairs = [] + events = el.get('events') + if events: + for ev_name, val in events.items(): + handler = '' if val is None else str(val) + if not handler: + handler = get_handler_name(element_name, ev_name) + pairs.append((ev_name, handler)) + elif el.get('on'): + handlers = el.get('handlers') or {} + for evt in el['on']: + evt_name = str(evt) + if handlers.get(evt_name): + handler = str(handlers[evt_name]) + else: + handler = get_handler_name(element_name, evt_name) + pairs.append((evt_name, handler)) + return pairs + + +# Проверить, подключено ли событие к элементу (в любом из форматов). +def test_element_event(el, event_name): + events = el.get('events') + if events and event_name in events: + return True + return event_name in (el.get('on') or []) + + def emit_events(lines, el, element_name, indent, type_key): - if not el.get('on'): + pairs = get_event_pairs(el, element_name) + if not pairs: return # Validate event names if type_key and type_key in KNOWN_EVENTS: allowed = KNOWN_EVENTS[type_key] - for evt in el['on']: - if allowed and str(evt) not in allowed: - print(f"[WARN] Unknown event '{evt}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") + for ev_name, _ in pairs: + if allowed and str(ev_name) not in allowed: + print(f"[WARN] Unknown event '{ev_name}' for {type_key} '{element_name}'. Known: {', '.join(allowed)}") lines.append(f"{indent}") - for evt in el['on']: - evt_name = str(evt) - handlers = el.get('handlers') - if handlers and handlers.get(evt_name): - handler = str(handlers[evt_name]) - else: - handler = get_handler_name(element_name, evt_name) - lines.append(f'{indent}\t{handler}') + for ev_name, handler in pairs: + lines.append(f'{indent}\t{handler}') lines.append(f"{indent}") @@ -2015,7 +2042,7 @@ def emit_input(lines, el, name, eid, indent): lines.append(f'{inner}true') if el.get('choiceButton') is False: lines.append(f'{inner}false') - elif el.get('choiceButton') is True and 'StartChoice' in (el.get('on') or []): + elif el.get('choiceButton') is True and test_element_event(el, 'StartChoice'): lines.append(f'{inner}true') if el.get('clearButton') is True: lines.append(f'{inner}true') diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 9111f022..b4e0da35 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.11 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.12 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -212,39 +212,19 @@ function Add-TitleLocation { elseif ($tl -ne $smartDefault) { $obj['titleLocation'] = $tl.ToLower() } } -# Суффиксы авто-имён обработчиков (инверсия компилятора) -$HANDLER_SUFFIX = @{ - 'OnChange'='ПриИзменении'; 'StartChoice'='НачалоВыбора'; 'ChoiceProcessing'='ОбработкаВыбора'; - 'AutoComplete'='АвтоПодбор'; 'Clearing'='Очистка'; 'Opening'='Открытие'; 'Click'='Нажатие'; - 'OnActivateRow'='ПриАктивизацииСтроки'; 'BeforeAddRow'='ПередНачаломДобавления'; - 'BeforeDeleteRow'='ПередУдалением'; 'BeforeRowChange'='ПередНачаломИзменения'; - 'OnStartEdit'='ПриНачалеРедактирования'; 'OnEndEdit'='ПриОкончанииРедактирования'; - 'Selection'='ВыборСтроки'; 'OnCurrentPageChange'='ПриСменеСтраницы'; 'TextEditEnd'='ОкончаниеВводаТекста'; - 'URLProcessing'='ОбработкаНавигационнойСсылки'; 'DragStart'='НачалоПеретаскивания'; 'Drag'='Перетаскивание'; - 'DragCheck'='ПроверкаПеретаскивания'; 'Drop'='Помещение'; 'AfterDeleteRow'='ПослеУдаления' -} - -# Разобрать элемента → { on:[...], handlers:{...} } с учётом авто-имён +# Разобрать элемента → упорядоченная мапа { ИмяСобытия: ИмяОбработчика } +# в порядке документа. Имена обработчиков всегда явные (как у событий формы) — +# единый, консистентный с form-level формат. Legacy on/handlers больше не эмитим. function Get-Events { param($node, [string]$elName) $ev = $node.SelectSingleNode("lf:Events", $ns) if (-not $ev) { return $null } - $on = New-Object System.Collections.ArrayList - $handlers = [ordered]@{} - # `on` — полный список событий в порядке документа (контракт DSL: on = массив имён событий); - # `handlers` — только переопределение имени, когда обработчик не выводится из авто-суффикса. + $events = [ordered]@{} foreach ($e in @($ev.SelectNodes("lf:Event", $ns))) { - $evName = $e.GetAttribute("name") - $handler = $e.InnerText - $auto = if ($HANDLER_SUFFIX.ContainsKey($evName) -and $elName) { "$elName$($HANDLER_SUFFIX[$evName])" } else { $null } - [void]$on.Add($evName) - if (-not ($auto -and $handler -eq $auto)) { $handlers[$evName] = $handler } + $events[$e.GetAttribute("name")] = $e.InnerText } - $res = [ordered]@{} - if ($on.Count -gt 0) { $res['on'] = @($on) } - if ($handlers.Count -gt 0) { $res['handlers'] = $handlers } - if ($res.Count -eq 0) { return $null } - return $res + if ($events.Count -eq 0) { return $null } + return $events } # Общие свойства элемента (visible/enabled/readonly/title/events) → в hash @@ -260,10 +240,7 @@ function Add-CommonProps { # formatted у LabelDecoration выводится компилятором из hyperlink — отдельный ключ не нужен (#16 хвост) } $ev = Get-Events $node $elName - if ($ev) { - if ($ev.Contains('on')) { $obj['on'] = $ev['on'] } - if ($ev.Contains('handlers')) { $obj['handlers'] = $ev['handlers'] } - } + if ($ev) { $obj['events'] = $ev } } # --- 3. Type decompile (inverse of Emit-Type) --- diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 59cc1750..fd841016 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -79,7 +79,7 @@ } ``` -Ключ — имя события, значение — имя процедуры-обработчика. +Ключ — имя события, значение — имя процедуры-обработчика. **Тот же формат `events` используется и на элементах** (§4.1) — единый способ описания событий во всём DSL. ### Доступные события @@ -114,8 +114,7 @@ | `hidden` | bool | `true` → `false` | | `disabled` | bool | `true` → `false` | | `readOnly` | bool | `true` → `true` | -| `on` | string[] | Массив имён событий | -| `handlers` | object | Явные имена обработчиков: `{"OnChange": "МойОбработчик"}` | +| `events` | object | Обработчики событий: `{ "ИмяСобытия": "ИмяОбработчика" }` — тот же формат, что у событий формы (§3). Значение `null` → имя по конвенции (§4.2). См. §4.2 | ### 4.1a. Общие layout-свойства @@ -136,14 +135,25 @@ | `horizontalAlign` | `` | `Left`, `Center`, `Right` | | `skipOnInput` | `` | `true` | -### 4.2. Автоименование обработчиков +### 4.2. События элемента и автоименование обработчиков -При указании `"on"` без `"handlers"` имя обработчика генерируется автоматически: +События элемента описываются мапой `events` (как у формы): +```json +{ "input": "Контрагент", "path": "Объект.Контрагент", + "events": { "OnChange": "КонтрагентПриИзменении" } } ``` -<ИмяЭлемента><РусскийСуффикс> + +Значение — имя процедуры-обработчика. Если вместо имени указать **`null`**, имя +генерируется автоматически по конвенции 1С `<ИмяЭлемента><РусскийСуффикс>`: + +```json +{ "input": "Контрагент", "path": "Объект.Контрагент", + "events": { "OnChange": null } } // → обработчик КонтрагентПриИзменении ``` +Суффиксы для авто-имени: + | Событие | Суффикс | |---------|---------| | `OnChange` | `ПриИзменении` | @@ -163,7 +173,10 @@ | `OnCurrentPageChange` | `ПриСменеСтраницы` | | `TextEditEnd` | `ОкончаниеВводаТекста` | -Пример: элемент `Контрагент` + событие `OnChange` → обработчик `КонтрагентПриИзменении`. +> **Legacy-формат (принимается, но устарел).** Ранее события элемента задавались парой +> `on` (массив имён событий) + `handlers` (переопределение имён): `{ "on": ["OnChange"], "handlers": { … } }`. +> Компилятор по-прежнему его принимает ради совместимости, но рекомендуемый и +> единственный эмитируемый формат — мапа `events`. Новые формы пишите через `events`. ### 4.3. Типы элементов @@ -184,7 +197,7 @@ #### input — InputField ```json -{ "input": "Организация", "path": "Объект.Организация", "on": ["OnChange"] } +{ "input": "Организация", "path": "Объект.Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } } ``` | Свойство | Тип | Описание | @@ -211,7 +224,7 @@ #### check — CheckBoxField ```json -{ "check": "ФлагАктивности", "path": "Активен", "on": ["OnChange"] } +{ "check": "ФлагАктивности", "path": "Активен", "events": { "OnChange": "ФлагАктивностиПриИзменении" } } ``` | Свойство | Тип | Описание | diff --git a/tests/skills/cases/form-compile/events.json b/tests/skills/cases/form-compile/events.json index 05bd48b0..7396e6db 100644 --- a/tests/skills/cases/form-compile/events.json +++ b/tests/skills/cases/form-compile/events.json @@ -17,9 +17,9 @@ "title": "События", "events": { "OnCreateAtServer": "ПриСозданииНаСервере", "OnOpen": "ПриОткрытии" }, "elements": [ - { "input": "Организация", "path": "Организация", "on": ["OnChange", "StartChoice"] }, - { "input": "Период", "path": "Период", "handlers": { "OnChange": "ПериодПриИзменении" } }, - { "label": "Подсказка", "title": "Нажмите для перехода", "hyperlink": true, "on": ["Click"] } + { "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении", "StartChoice": "ОрганизацияНачалоВыбора" } }, + { "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } }, + { "label": "Подсказка", "title": "Нажмите для перехода", "hyperlink": true, "events": { "Click": null } } ], "attributes": [ { "name": "Объект", "type": "DataProcessorObject.События", "main": true }, diff --git a/tests/skills/cases/form-compile/snapshots/events/DataProcessors/События/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/events/DataProcessors/События/Forms/Форма/Ext/Form.xml index 4621a24e..8aa8561f 100644 --- a/tests/skills/cases/form-compile/snapshots/events/DataProcessors/События/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/events/DataProcessors/События/Forms/Форма/Ext/Form.xml @@ -26,6 +26,9 @@ Период + + ПериодПриИзменении +