griffincss-ui 0.25.0-dev → 0.26.2

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.
Files changed (49) hide show
  1. package/dist/griffincss-ui-scoped.css +1 -1
  2. package/dist/griffincss-ui.css +1 -1
  3. package/dist/griffincss-ui.js +1 -1
  4. package/dist/griffincss-ui.js.map +1 -1
  5. package/dist/griffinjs-core.js +1 -1
  6. package/dist/griffinjs-core.js.map +1 -1
  7. package/dist/griffinjs-dialog.js +1 -1
  8. package/dist/griffinjs-dialog.js.map +1 -1
  9. package/dist/griffinjs-dropdown.js +1 -1
  10. package/dist/griffinjs-dropdown.js.map +1 -1
  11. package/dist/griffinjs-fields.css +1 -1
  12. package/dist/griffinjs-fields.js +1 -1
  13. package/dist/griffinjs-fields.js.map +1 -1
  14. package/dist/griffinjs-loader.js +3 -0
  15. package/dist/griffinjs-loader.js.map +1 -0
  16. package/dist/griffinjs-megamenu.js +1 -1
  17. package/dist/griffinjs-megamenu.js.map +1 -1
  18. package/dist/griffinjs-validate.js +1 -1
  19. package/dist/griffinjs-validate.js.map +1 -1
  20. package/dist/griffinjs.css +1 -1
  21. package/dist/griffinjs.js +1 -1
  22. package/dist/griffinjs.js.map +1 -1
  23. package/package.json +4 -3
  24. package/scss/_accordion.scss +5 -2
  25. package/scss/_alert.scss +1 -1
  26. package/scss/_card.scss +33 -7
  27. package/scss/_choice.scss +81 -10
  28. package/scss/_dropdown.scss +7 -3
  29. package/scss/_form.scss +42 -1
  30. package/scss/_menu.scss +4 -1
  31. package/scss/_modal.scss +44 -7
  32. package/scss/_nav.scss +5 -2
  33. package/scss/_pagination.scss +8 -1
  34. package/scss/_rating.scss +7 -1
  35. package/scss/_table.scss +91 -0
  36. package/scss/_tabs.scss +4 -1
  37. package/scss/griffincss-ui-scoped.scss +17 -7
  38. package/scss/griffincss-ui.scss +16 -10
  39. package/scss/griffinjs/_track.scss +43 -4
  40. package/scss/griffinjs/fields/_number.scss +9 -11
  41. package/scss/griffinjs/fields/fields.scss +1 -1
  42. package/scss/griffinjs/griffinjs.scss +1 -1
  43. package/src/griffincss-ui.js +2 -2
  44. package/src/griffinjs/core/griffinjs-core.js +57 -11
  45. package/src/griffinjs/core/loader.js +124 -0
  46. package/src/griffinjs/fields/validate.js +24 -3
  47. package/src/griffinjs/widgets/dialog.js +77 -2
  48. package/src/griffinjs/widgets/dropdown.js +94 -4
  49. package/src/griffinjs/widgets/megamenu.js +90 -5
@@ -10,6 +10,7 @@
10
10
  // Движение — здесь. JS пишет только aria-*, data-gr-state и --gr-progress.
11
11
 
12
12
  @use 'griffincss-core/scss/breakpoints' as bp;
13
+ @use 'griffincss-core/scss/container-mixins' as cq;
13
14
 
14
15
  .gr-track {
15
16
  display: flex;
@@ -45,12 +46,50 @@
45
46
  }
46
47
  }
47
48
 
49
+ // Доля слайда описана один раз: без суффикса — база, с суффиксом
50
+ // брейкпоинта — от ширины окна, с буквой c — от ширины ближайшего предка
51
+ // с .gr-cq (Этап 47d): виджет в слоте меряется слотом. Виджету дорожки
52
+ // всё равно, откуда пришла доля — perView он считает по геометрии.
53
+ @mixin gr-track-per-view($suffix) {
54
+ @each $n in (1, 2, 3, 4) {
55
+ .gr-track-#{$n}#{$suffix} > * {
56
+ flex-basis: calc((100% - var(--gr-gap) * #{$n - 1}) / #{$n});
57
+ }
58
+ }
59
+ }
60
+
48
61
  @each $bp-name, $bp-value in bp.$gr-breakpoints {
49
62
  @media (width >= $bp-value) {
50
- @each $n in (1, 2, 3, 4) {
51
- .gr-track-#{$n}-#{$bp-name} > * {
52
- flex-basis: calc((100% - var(--gr-gap) * #{$n - 1}) / #{$n});
53
- }
63
+ @include gr-track-per-view('-#{$bp-name}');
64
+ }
65
+ }
66
+
67
+ @each $bp-name, $bp-value in bp.$gr-breakpoints {
68
+ @include cq.gr-container-media($bp-name) {
69
+ @include gr-track-per-view('-c#{$bp-name}');
70
+ }
71
+ }
72
+
73
+ // Лента столбиком в узком гнезде (Этап 47d, фидбек темы griffin, п. 8):
74
+ // подборка товаров в боковой колонке — список, в содержимом — лента.
75
+ // Порог — ширина ближайшего предка с .gr-cq, суффикс называет его
76
+ // (как у .gr-table-stack-c*): .gr-track-stack-csm — столбик, пока гнездо
77
+ // уже 640px. Оконных порогов нет: в узком окне лента остаётся лентой,
78
+ // свайп и есть её мобильный режим. Слайды отпускаются по главной оси —
79
+ // flex-basis: auto бьёт долю .gr-track-N > * тем, что стоит позже: между
80
+ // 640 и 768 px гнезда .gr-track-2-csm и .gr-track-stack-cmd совпадают,
81
+ // и столбик обязан стоять в файле после долей.
82
+ // Слайдеру не подходит: его кнопки и точки останутся на месте.
83
+ @each $bp-name, $bp-value in bp.$gr-breakpoints {
84
+ @include cq.gr-container-below($bp-name) {
85
+ .gr-track-stack-c#{$bp-name} {
86
+ flex-direction: column;
87
+ overflow-x: visible;
88
+ scroll-snap-type: none;
89
+ }
90
+
91
+ .gr-track-stack-c#{$bp-name} > * {
92
+ flex-basis: auto;
54
93
  }
55
94
  }
56
95
  }
@@ -1,12 +1,10 @@
1
1
  // GriffinJS — сумма с разрядами (Этап 43b)
2
- // Своих стилей у поля нет: на экране обычный .gr-input с текстом. Единственное
3
- // правило — про спутник. Скрытый input с числом встаёт сразу за полем
4
- // (у суммы — всегда, у даты — на указателе мыши), и в .gr-input-group
5
- // с аддоном после поля он был бы последним ребёнком: поле теряло бы
6
- // скругление края. Скругление возвращается тому, за кем стоит спутник.
7
- // Без :has() остаётся прямой угол у поля — деградация видимая, но мягкая.
8
-
9
- .gr-input-group > :has(+ [hidden]:last-child) {
10
- border-start-end-radius: var(--gr-radius);
11
- border-end-end-radius: var(--gr-radius);
12
- }
2
+ // Своих стилей у поля нет: на экране обычный .gr-input с текстом. Скрытый
3
+ // спутник с числом встаёт сразу за полем (у суммы — всегда, у даты —
4
+ // на указателе мыши), и в .gr-input-group с аддоном после поля он был бы
5
+ // последним ребёнком: до Этапа 45 поле теряло бы скругление края, и его
6
+ // возвращало правило `:has(+ [hidden]:last-child)` здесь. Теперь края
7
+ // группы считаются по видимым полям в самом компоненте (_form.scss,
8
+ // `:nth-last-child(1 of S)`), и спутник, как любой скрытый узел, края
9
+ // не трогает. Файл остаётся в списке сборки пустым: место для стилей поля,
10
+ // если они понадобятся, — здесь, а не в чужом модуле.
@@ -14,7 +14,7 @@
14
14
 
15
15
 
16
16
  @use 'sass:meta';
17
- @layer griffincss.reset, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style;
17
+ @layer griffincss.reset, griffincss.tokens, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style, griffincss.hidden;
18
18
 
19
19
  // Весь вывод — в griffincss.ui, как у компонентов.
20
20
  @layer griffincss.ui {
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  @use 'sass:meta';
21
- @layer griffincss.reset, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style;
21
+ @layer griffincss.reset, griffincss.tokens, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style, griffincss.hidden;
22
22
 
23
23
  // Весь вывод слоя — в griffincss.ui, как у компонентов.
24
24
  @layer griffincss.ui {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Griffincss UI — Runtime v0.25.0-dev
2
+ * Griffincss UI — Runtime v0.26.2
3
3
  * Опциональный рантайм пакета компонентов. Делает ровно то, чего платформа
4
4
  * не даёт вовсе; всё, что умеют <details>, <dialog> и Popover API, остаётся
5
5
  * за ними. Без этого файла компоненты работают — просто без перечисленного.
@@ -50,7 +50,7 @@
50
50
  })(function () {
51
51
  'use strict';
52
52
 
53
- var VERSION = '0.25.0-dev';
53
+ var VERSION = '0.26.2';
54
54
 
55
55
  var OVERLAY_ATTR = 'data-gr-overlay-close';
56
56
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * GriffinJS — Core v0.25.0-dev
2
+ * GriffinJS — Core v0.26.2
3
3
  * Опциональный слой виджетов с состоянием поверх Griffincss: слайдер,
4
4
  * галерея, лайтбокс, параллакс, мегаменю. Подключается одной строкой
5
5
  * и одним файлом; страница без него — законное и рабочее состояние.
@@ -10,8 +10,8 @@
10
10
  * поднимает всё, destroy() снимает всё до последнего атрибута.
11
11
  *
12
12
  * В griffinjs-core.js склеивается только то, без чего не обходится ни один
13
- * виджет: options, registry, scanner. Остальное из core/ — anchor, media,
14
- * motion, gesture, track — общие части ВНЕ ядра (Этап 22): ими пользуется
13
+ * виджет: options, registry, scanner. Остальное из core/ — loader, anchor,
14
+ * media, motion, gesture, track — общие части ВНЕ ядра (Этап 22): ими пользуется
15
15
  * меньшинство, и каждая уезжает своим файлом griffinjs-<часть>.js. Что
16
16
  * модуль из этого берёт, он объявляет сам — вторым аргументом defineWidget
17
17
  * или needs(); start() сверяет объявленное со сборкой.
@@ -20,6 +20,13 @@
20
20
  * window.GriffinJS, а не window.Griffincss — там слияние рантаймов
21
21
  * CSS-части, и порядок тегов не значим; слою с состоянием чужой
22
22
  * глобал ни к чему.
23
+ *
24
+ * Участник жизненного цикла (use) кроме start/destroy может объявить
25
+ * init(root): ядро зовёт его после подъёма виджетов в корне — и на старте,
26
+ * и на каждой порции сканера. Так догрузка бандла полей (core/loader.js,
27
+ * Этап 48c) видит data-gr-<поле> без виджета, не будучи частью ядра.
28
+ * GriffinJS.config — общий объект настроек: ядру он не нужен, но это
29
+ * его поверхность, и модуль читает из неё (config.fields у загрузчика).
23
30
  */
24
31
  (function (factory) {
25
32
  'use strict';
@@ -38,7 +45,7 @@
38
45
  })(function () {
39
46
  'use strict';
40
47
 
41
- var VERSION = '0.25.0-dev';
48
+ var VERSION = '0.26.2';
42
49
 
43
50
  // Реестры. Модуль зовёт defineEngine/defineWidget; ядро — единственное
44
51
  // место, которое знает, как их применить.
@@ -61,6 +68,9 @@
61
68
 
62
69
  var started = false;
63
70
 
71
+ // Настройки — одна поверхность на слой; ядро само ничего отсюда не читает.
72
+ var config = {};
73
+
64
74
  // Объявление и чтение зависимостей: needs('slider', ['track', 'motion'])
65
75
  // записывает, needs('slider') возвращает.
66
76
  function needs(name, list) {
@@ -273,6 +283,10 @@
273
283
  for (var j = 0; j < nodes.length; j++) mount(nodes[j], name);
274
284
  }
275
285
 
286
+ for (var k = 0; k < lifecycle.length; k++) {
287
+ if (typeof lifecycle[k].init === 'function') lifecycle[k].init(root);
288
+ }
289
+
276
290
  return api;
277
291
  }
278
292
 
@@ -332,22 +346,54 @@
332
346
 
333
347
  // Автостарт: только в браузере и только если его не отключили атрибутом
334
348
  // data-auto="false" на теге <script>.
349
+ //
350
+ // Момент старта — когда поставка зарегистрирована целиком. Ядро — первый
351
+ // IIFE бандла, и старт при выполнении собственного файла обгоняет
352
+ // defineWidget модулей дальше в том же файле; под defer так и было:
353
+ // readyState уже «interactive», старт шёл сразу, init(document) не
354
+ // находил ни одного виджета, поздних никто не поднимал (фидбек темы
355
+ // griffin по 0.26.0, п. 1). Три момента:
356
+ // * разбор идёт (loading) — DOMContentLoaded, как прежде;
357
+ // * свой тег отложен (defer) — тоже DOMContentLoaded: отложенные теги
358
+ // выполняются после разбора и ДО события, значит к нему выполнятся
359
+ // и griffinjs-fields.js с griffinjs-countries.js следующими тегами —
360
+ // та же схема, что синхронные теги в подвале. Не readystatechange:
361
+ // «interactive» наступает ДО отложенных тегов;
362
+ // * иначе (документ готов, тег вставлен скриптом) — микрозадачей:
363
+ // после текущего файла целиком, до любого следующего тега. Набору
364
+ // «ядро + нужное», вставленному скриптом, нужен data-auto="false"
365
+ // и GriffinJS.start() после последнего тега.
366
+ // Тег с defer, вставленный скриптом после DOMContentLoaded, отложенным
367
+ // не является, и события он не дождётся; страховка от вечного
368
+ // ожидания — readystatechange: при «interactive» следующее — «complete».
335
369
  function autoStart() {
336
- try {
337
- var script = document.currentScript;
338
- if (script && script.getAttribute('data-auto') === 'false') return;
339
- } catch (e) { /* currentScript недоступен */ }
370
+ var script = null;
371
+
372
+ try { script = document.currentScript; } catch (e) { /* currentScript недоступен */ }
373
+
374
+ if (script && script.getAttribute('data-auto') === 'false') return;
375
+
376
+ var deferred = !!script && script.hasAttribute('defer') && !script.hasAttribute('async');
377
+
378
+ if (document.readyState === 'loading' || (deferred && document.readyState !== 'complete')) {
379
+ var ready = function () {
380
+ document.removeEventListener('DOMContentLoaded', ready);
381
+ document.removeEventListener('readystatechange', ready);
382
+ start();
383
+ };
384
+
385
+ document.addEventListener('DOMContentLoaded', ready);
386
+ if (deferred) document.addEventListener('readystatechange', ready);
340
387
 
341
- if (document.readyState === 'loading') {
342
- document.addEventListener('DOMContentLoaded', function () { start(); });
343
388
  return;
344
389
  }
345
390
 
346
- start();
391
+ Promise.resolve().then(start);
347
392
  }
348
393
 
349
394
  var api = {
350
395
  version: VERSION,
396
+ config: config,
351
397
  engines: engines,
352
398
  widgets: widgets,
353
399
  defineEngine: defineEngine,
@@ -0,0 +1,124 @@
1
+ /*
2
+ * GriffinJS — догрузка бандла полей по потребности (Этап 48c).
3
+ *
4
+ * <script defer src="griffinjs.js"
5
+ * data-fields="griffinjs-fields.js"
6
+ * data-countries="griffinjs-countries.js"></script>
7
+ *
8
+ * GriffinJS.config.fields = { src: 'griffinjs-fields.js', countries: 'griffinjs-countries.js' };
9
+ *
10
+ * Витрине поля нужны на трёх страницах из трёхсот, а тег бандла стоит
11
+ * в шаблоне шапки для всех. Вместо тега — путь: встретив data-gr-<поле>
12
+ * без зарегистрированного виджета, слой вставляет <script> сам — один раз
13
+ * на документ, с nonce со своего тега (под строгим CSP без него файл
14
+ * отброшен молча), таблица стран вторым тегом в том же порядке, — и после
15
+ * загрузки ВСЕХ файлов поднимает поля по документу и шлёт
16
+ * griffinjs:fields-loaded. Ошибка загрузки — предупреждение, поля остаются
17
+ * базой без скрипта, второй попытки нет. Без config — предупреждение раз
18
+ * на имя поля: раньше такое поле молчало.
19
+ *
20
+ * Единственное, что здесь известно поимённо, — не виджет, а файл поставки:
21
+ * девять имён второго бандла, те же, что в списке FIELDS сборки
22
+ * (scripts/build-griffinjs.mjs; тест сверяет). Ядро о них не знает:
23
+ * модуль подписывается на init(root) и лежит вне ядра, в SHARED, —
24
+ * за него не платит набор из двух файлов ради одной подсказки.
25
+ *
26
+ * Пути и nonce читаются у своего тега в момент выполнения — позже
27
+ * document.currentScript уже null. Атрибуты, а не только свойство, —
28
+ * ради тега с defer: инлайновый скрипт перед ним глобала ещё не видит.
29
+ */
30
+ (function (G) {
31
+ 'use strict';
32
+
33
+ var FIELDS = ['mask', 'phone', 'datetime', 'number', 'file', 'rating', 'otp', 'counter', 'validate'];
34
+
35
+ var config = G.config;
36
+ var nonce = '';
37
+ var loading = null; // null — не запрашивали; pending | done | failed
38
+ var warned = {}; // поле → без config уже сказано
39
+
40
+ try {
41
+ var script = document.currentScript;
42
+
43
+ if (script) {
44
+ nonce = script.nonce || '';
45
+
46
+ var src = script.getAttribute('data-fields');
47
+
48
+ if (src) config.fields = { src: src, countries: script.getAttribute('data-countries') || '' };
49
+ }
50
+ } catch (e) { /* currentScript недоступен */ }
51
+
52
+ // Поля в корне, чьих виджетов нет: с config — запрос бандла, без него —
53
+ // предупреждение. Узлы не трогаются: база без скрипта — рабочее состояние.
54
+ function check(root) {
55
+ for (var i = 0; i < FIELDS.length; i++) {
56
+ var name = FIELDS[i];
57
+ var selector = '[data-gr-' + name + ']';
58
+
59
+ if (G.widgets[name] || warned[name]) continue;
60
+ if (!(root.matches && root.matches(selector)) && !(root.querySelector && root.querySelector(selector))) continue;
61
+
62
+ if (config.fields && config.fields.src) return request();
63
+
64
+ warned[name] = true;
65
+ G.warn('поле «' + name + '» не собрано: подключите griffinjs-fields.js или задайте GriffinJS.config.fields');
66
+ }
67
+ }
68
+
69
+ // Поля поднимаются после ВСЕХ файлов: телефон читает таблицу стран
70
+ // при монтировании, и она обязана лечь раньше. Что успело
71
+ // зарегистрироваться при ошибке, всё равно поднимается.
72
+ function request() {
73
+ if (loading) return;
74
+
75
+ loading = 'pending';
76
+
77
+ var files = [config.fields.src];
78
+ var ok = true;
79
+ var left;
80
+
81
+ if (config.fields.countries) files.push(config.fields.countries);
82
+
83
+ left = files.length;
84
+
85
+ function settle(event) {
86
+ if (event.type === 'error') {
87
+ ok = false;
88
+ G.warn('бандл полей не загружен: ' + event.target.src + ' — поля остались базой без скрипта');
89
+ }
90
+
91
+ if (--left) return;
92
+
93
+ loading = ok ? 'done' : 'failed';
94
+ G.init(document);
95
+
96
+ if (ok) G.emit('griffinjs:fields-loaded', document, { files: files });
97
+ }
98
+
99
+ for (var i = 0; i < files.length; i++) insert(files[i], settle);
100
+ }
101
+
102
+ // async = false: вставленный скриптом тег по умолчанию async, а порядок
103
+ // «поля, затем страны» нужен. nonce — свойством: атрибут после разбора
104
+ // браузер прячет.
105
+ function insert(src, settle) {
106
+ var tag = document.createElement('script');
107
+
108
+ tag.src = src;
109
+ tag.async = false;
110
+ if (nonce) tag.nonce = nonce;
111
+
112
+ tag.addEventListener('load', settle);
113
+ tag.addEventListener('error', settle);
114
+
115
+ (document.head || document.documentElement).appendChild(tag);
116
+ }
117
+
118
+ G.loader = {
119
+ _fields: function () { return FIELDS.slice(); },
120
+ _nonce: function (value) { if (value !== undefined) nonce = value; return nonce; }
121
+ };
122
+
123
+ G.use({ init: check });
124
+ })(typeof window !== 'undefined' && window.GriffinJS ? window.GriffinJS : require('./griffinjs-core.js'));
@@ -12,8 +12,10 @@
12
12
  * сообщение», где сообщение — платформенное, в языке браузера, а подпись —
13
13
  * <label> поля (или его aria-label, или name). Фокус после отправки —
14
14
  * на первой ссылке; Enter по ней ведёт к полю. Непройденные поля получают
15
- * aria-invalid="true" — их красит CSS семьи .gr-field, — и теряют его,
16
- * как только исправлены; список же обновляется следующей отправкой.
15
+ * aria-invalid="true" — источник для программы чтения с экрана, — а их
16
+ * обёртка .gr-field, если она есть, состояние data-gr-state="error":
17
+ * его и красит CSS семьи. Оба снимаются, как только поле исправлено;
18
+ * список же обновляется следующей отправкой.
17
19
  *
18
20
  * Пузырь браузера на время жизни виджета снимается (novalidate): двум
19
21
  * сообщениям об одном и том же незачем спорить. Проверку это не отменяет —
@@ -84,9 +86,26 @@
84
86
  return control.getAttribute('id');
85
87
  }
86
88
 
89
+ function fieldOf(control) {
90
+ return G.closest(control, '.gr-field');
91
+ }
92
+
93
+ // Обёртка держит состояние, пока хотя бы один её контрол не пройден.
94
+ function release(control) {
95
+ var field = fieldOf(control);
96
+
97
+ for (var i = 0; field && i < marked.length; i++) if (fieldOf(marked[i]) === field) return;
98
+ if (field) attrs.set(field, 'data-gr-state', null);
99
+ }
100
+
87
101
  function unmark() {
88
- for (var i = 0; i < marked.length; i++) attrs.set(marked[i], 'aria-invalid', null);
102
+ var was = marked;
103
+
89
104
  marked = [];
105
+ for (var i = 0; i < was.length; i++) {
106
+ attrs.set(was[i], 'aria-invalid', null);
107
+ release(was[i]);
108
+ }
90
109
  }
91
110
 
92
111
  function render(failed) {
@@ -112,6 +131,7 @@
112
131
  item.appendChild(link);
113
132
  list.appendChild(item);
114
133
  attrs.set(failed[i], 'aria-invalid', 'true');
134
+ if (fieldOf(failed[i])) attrs.set(fieldOf(failed[i]), 'data-gr-state', 'error');
115
135
  marked.push(failed[i]);
116
136
  }
117
137
 
@@ -165,6 +185,7 @@
165
185
  if (at !== -1 && !invalid(control)) {
166
186
  attrs.set(control, 'aria-invalid', null);
167
187
  marked.splice(at, 1);
188
+ release(control);
168
189
  }
169
190
  }
170
191
 
@@ -21,7 +21,14 @@
21
21
  * * ajax-содержимое: data-gr-src грузится один раз в [data-gr-content]
22
22
  * (или в само окно), состояния loading → open | error;
23
23
  * * hash: открытие пишет #id в историю, «Назад» закрывает окно,
24
- * а страница, открытая с #id, показывает окно сразу.
24
+ * а страница, открытая с #id, показывает окно сразу;
25
+ * * рост окна (Этап 48d) — data-gr-dialog="grow": ResizeObserver
26
+ * на окне, и смена высоты больше 24 px идёт анимацией от прежней
27
+ * к новой (element.animate по block-size — не спорит с transition
28
+ * компонента). Для окна, чьё содержимое приходит ajax'ом: плашка
29
+ * в 10rem не прыгает к форме, а вырастает до неё. Только по атрибуту:
30
+ * наблюдатель на каждом окне — цена, которую не все просили. Открытие
31
+ * и закрытие не трогаются, prefers-reduced-motion — без анимации.
25
32
  *
26
33
  * Разметка и стили .gr-modal / .gr-drawer не меняются. Окно поднимается
27
34
  * по data-gr-dialog или при первом открытии через data-gr-open — атрибут
@@ -33,9 +40,35 @@
33
40
  var DEFAULTS = {
34
41
  hash: false,
35
42
  media: true,
36
- lock: true
43
+ lock: true,
44
+ grow: false
37
45
  };
38
46
 
47
+ // Порог роста: ниже — ввод в textarea с автовысотой, а не приход содержимого.
48
+ var GROW_STEP = 24;
49
+
50
+ function reducedMotion() {
51
+ try {
52
+ return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
53
+ } catch (e) { return false; }
54
+ }
55
+
56
+ // Темп окна, мс: --gr-modal-transition темы, иначе --gr-transition;
57
+ // «0.2s ease» → 200, пусто или 0s → 0.
58
+ function duration(node) {
59
+ var value = '';
60
+
61
+ try {
62
+ var styles = getComputedStyle(node);
63
+
64
+ value = styles.getPropertyValue('--gr-modal-transition') || styles.getPropertyValue('--gr-transition');
65
+ } catch (e) { /* без CSSOM */ }
66
+
67
+ var m = /(\d*\.?\d+)(m?s)/.exec(value);
68
+
69
+ return m ? parseFloat(m[1]) * (m[2] === 's' ? 1000 : 1) : 0;
70
+ }
71
+
39
72
  var OPEN_ATTR = 'data-gr-open';
40
73
 
41
74
  var stack = []; // открытые окна, последнее — верхнее
@@ -103,6 +136,9 @@
103
136
  var frames = []; // [{el, src}] — разгруженные iframe
104
137
  var byHistory = false; // закрыто кнопкой «Назад»: back() не зовём
105
138
  var record = { el: el, o: o };
139
+ var grow = null; // ResizeObserver — только по grow
140
+ var known = 0; // высота окна по последнему наблюдению
141
+ var growing = null; // идущая анимация высоты
106
142
 
107
143
  function id() { return el.getAttribute('id') || ''; }
108
144
 
@@ -167,6 +203,38 @@
167
203
  });
168
204
  }
169
205
 
206
+ // --- Рост окна ----------------------------------------------------------
207
+
208
+ // Открытие и закрытие (0 ↔ высота) — не рост; мелочь до порога — тоже.
209
+ // Анимация меняет размер самого окна, поэтому на её время окно снято
210
+ // с наблюдения: начатая здесь при живом наблюдении, она давала на window
211
+ // «ResizeObserver loop completed with undelivered notifications».
212
+ // Промежуточные высоты — её же, слушать их незачем. Вернувшись,
213
+ // наблюдение первым сообщает текущую высоту: если за время анимации
214
+ // пришло ещё содержимое, окно дорастёт следующей.
215
+ function onResize() {
216
+ var next = el.getBoundingClientRect().height;
217
+ var prev = known;
218
+
219
+ known = next;
220
+
221
+ if (!prev || !next || !isOpen() || Math.abs(next - prev) <= GROW_STEP) return;
222
+
223
+ var ms = reducedMotion() ? 0 : duration(el);
224
+
225
+ if (!ms || typeof el.animate !== 'function') return;
226
+
227
+ grow.unobserve(el);
228
+ growing = el.animate([{ blockSize: prev + 'px' }, { blockSize: next + 'px' }], { duration: ms, easing: 'ease' });
229
+ growing.onfinish = growing.oncancel = settle;
230
+ }
231
+
232
+ // После destroy() наблюдателя нет: отмена приходит позже и не вернёт его.
233
+ function settle() {
234
+ growing = null;
235
+ if (grow) grow.observe(el);
236
+ }
237
+
170
238
  // --- Hash ---------------------------------------------------------------
171
239
 
172
240
  function hashIs() {
@@ -242,6 +310,8 @@
242
310
  var at = stack.indexOf(record);
243
311
  if (at !== -1) stack.splice(at, 1);
244
312
 
313
+ if (grow) { grow.disconnect(); grow = null; }
314
+ if (growing) growing.cancel();
245
315
  relock();
246
316
  restoreMedia();
247
317
  events.removeAll();
@@ -260,6 +330,11 @@
260
330
  listen(el, 'close', onClose);
261
331
  setAttr(el, 'data-gr-state', isOpen() ? 'open' : 'ready');
262
332
 
333
+ if (o.grow && typeof ResizeObserver === 'function') {
334
+ grow = new ResizeObserver(onResize);
335
+ grow.observe(el);
336
+ }
337
+
263
338
  if (isOpen()) { stack.push(record); relock(); }
264
339
 
265
340
  // Страница открыта с #id — окно показывается сразу.