griffincss-ui 0.25.0-dev → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/griffincss-ui-scoped.css +1 -1
- package/dist/griffincss-ui.css +1 -1
- package/dist/griffincss-ui.js +1 -1
- package/dist/griffincss-ui.js.map +1 -1
- package/dist/griffinjs-core.js +1 -1
- package/dist/griffinjs-core.js.map +1 -1
- package/dist/griffinjs-dialog.js +1 -1
- package/dist/griffinjs-dialog.js.map +1 -1
- package/dist/griffinjs-dropdown.js +1 -1
- package/dist/griffinjs-dropdown.js.map +1 -1
- package/dist/griffinjs-fields.css +1 -1
- package/dist/griffinjs-fields.js +1 -1
- package/dist/griffinjs-fields.js.map +1 -1
- package/dist/griffinjs-loader.js +3 -0
- package/dist/griffinjs-loader.js.map +1 -0
- package/dist/griffinjs-megamenu.js +1 -1
- package/dist/griffinjs-megamenu.js.map +1 -1
- package/dist/griffinjs-validate.js +1 -1
- package/dist/griffinjs-validate.js.map +1 -1
- package/dist/griffinjs.css +1 -1
- package/dist/griffinjs.js +1 -1
- package/dist/griffinjs.js.map +1 -1
- package/package.json +4 -3
- package/scss/_accordion.scss +5 -2
- package/scss/_alert.scss +1 -1
- package/scss/_card.scss +33 -7
- package/scss/_choice.scss +36 -4
- package/scss/_dropdown.scss +7 -3
- package/scss/_form.scss +42 -1
- package/scss/_menu.scss +4 -1
- package/scss/_modal.scss +44 -7
- package/scss/_nav.scss +5 -2
- package/scss/_rating.scss +7 -1
- package/scss/_table.scss +91 -0
- package/scss/_tabs.scss +4 -1
- package/scss/griffincss-ui-scoped.scss +17 -7
- package/scss/griffincss-ui.scss +16 -10
- package/scss/griffinjs/_track.scss +43 -4
- package/scss/griffinjs/fields/_number.scss +9 -11
- package/scss/griffinjs/fields/fields.scss +1 -1
- package/scss/griffinjs/griffinjs.scss +1 -1
- package/src/griffincss-ui.js +2 -2
- package/src/griffinjs/core/griffinjs-core.js +57 -11
- package/src/griffinjs/core/loader.js +124 -0
- package/src/griffinjs/fields/validate.js +24 -3
- package/src/griffinjs/widgets/dialog.js +67 -2
- package/src/griffinjs/widgets/dropdown.js +94 -4
- package/src/griffinjs/widgets/megamenu.js +90 -5
|
@@ -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 {
|
package/src/griffincss-ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Griffincss UI — Runtime v0.
|
|
2
|
+
* Griffincss UI — Runtime v0.26.1
|
|
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.
|
|
53
|
+
var VERSION = '0.26.1';
|
|
54
54
|
|
|
55
55
|
var OVERLAY_ATTR = 'data-gr-overlay-close';
|
|
56
56
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* GriffinJS — Core v0.
|
|
2
|
+
* GriffinJS — Core v0.26.1
|
|
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/ —
|
|
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.
|
|
48
|
+
var VERSION = '0.26.1';
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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" —
|
|
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
|
-
|
|
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,28 @@
|
|
|
167
203
|
});
|
|
168
204
|
}
|
|
169
205
|
|
|
206
|
+
// --- Рост окна ----------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
// Пока идёт анимация, наблюдение молчит: промежуточные высоты — её же.
|
|
209
|
+
// Открытие и закрытие (0 ↔ высота) — не рост; мелочь до порога — тоже.
|
|
210
|
+
function onResize() {
|
|
211
|
+
if (growing) return;
|
|
212
|
+
|
|
213
|
+
var next = el.getBoundingClientRect().height;
|
|
214
|
+
var prev = known;
|
|
215
|
+
|
|
216
|
+
known = next;
|
|
217
|
+
|
|
218
|
+
if (!prev || !next || !isOpen() || Math.abs(next - prev) <= GROW_STEP) return;
|
|
219
|
+
|
|
220
|
+
var ms = reducedMotion() ? 0 : duration(el);
|
|
221
|
+
|
|
222
|
+
if (!ms || typeof el.animate !== 'function') return;
|
|
223
|
+
|
|
224
|
+
growing = el.animate([{ blockSize: prev + 'px' }, { blockSize: next + 'px' }], { duration: ms, easing: 'ease' });
|
|
225
|
+
growing.onfinish = growing.oncancel = function () { growing = null; };
|
|
226
|
+
}
|
|
227
|
+
|
|
170
228
|
// --- Hash ---------------------------------------------------------------
|
|
171
229
|
|
|
172
230
|
function hashIs() {
|
|
@@ -242,6 +300,8 @@
|
|
|
242
300
|
var at = stack.indexOf(record);
|
|
243
301
|
if (at !== -1) stack.splice(at, 1);
|
|
244
302
|
|
|
303
|
+
if (grow) grow.disconnect();
|
|
304
|
+
if (growing) growing.cancel();
|
|
245
305
|
relock();
|
|
246
306
|
restoreMedia();
|
|
247
307
|
events.removeAll();
|
|
@@ -260,6 +320,11 @@
|
|
|
260
320
|
listen(el, 'close', onClose);
|
|
261
321
|
setAttr(el, 'data-gr-state', isOpen() ? 'open' : 'ready');
|
|
262
322
|
|
|
323
|
+
if (o.grow && typeof ResizeObserver === 'function') {
|
|
324
|
+
grow = new ResizeObserver(onResize);
|
|
325
|
+
grow.observe(el);
|
|
326
|
+
}
|
|
327
|
+
|
|
263
328
|
if (isOpen()) { stack.push(record); relock(); }
|
|
264
329
|
|
|
265
330
|
// Страница открыта с #id — окно показывается сразу.
|
|
@@ -21,10 +21,22 @@
|
|
|
21
21
|
* * позицию popover-панели у кнопки: считается здесь, а не CSS anchor
|
|
22
22
|
* positioning, — одинаково во всех движках (нужен griffinjs-anchor.js);
|
|
23
23
|
* * наведение с задержкой намерения (hover) — только для pointerType
|
|
24
|
-
* mouse/pen: касание открывает щелчком, как без
|
|
24
|
+
* mouse/pen: касание открывает щелчком, как без скрипта;
|
|
25
|
+
* * закрытие <details> с состоянием (Этап 48d): панель, чей уход тема
|
|
26
|
+
* анимирует, не пропадает в первом же кадре — обёртка получает
|
|
27
|
+
* data-gr-state="closing", open снимается по transitionend/animationend
|
|
28
|
+
* самой панели или по таймауту в длительность --gr-transition плюс
|
|
29
|
+
* 50 мс. Щелчок по кнопке открытого дропдауна идёт тем же путём.
|
|
30
|
+
* Во всех браузерах, без развилки по ::details-content: Firefox 153
|
|
31
|
+
* поддерживает селектор и allow-discrete, но закрытие не анимирует —
|
|
32
|
+
* замер в browser/closing.spec.mjs (решение владельца 2026-09-21).
|
|
33
|
+
* CSS слоя переходов не задаёт — при 0s закрывается сразу.
|
|
34
|
+
* Popover-панель анимирует платформа через display allow-discrete —
|
|
35
|
+
* ей это не нужно.
|
|
25
36
|
*
|
|
26
37
|
* Открытое состояние — data-gr-state="open" на обёртке: у варианта
|
|
27
38
|
* по наведению (.gr-dropdown-hover) панель по нему показывает CSS слоя.
|
|
39
|
+
* Между open и ready у <details> бывает closing.
|
|
28
40
|
*/
|
|
29
41
|
(function (G) {
|
|
30
42
|
'use strict';
|
|
@@ -70,6 +82,18 @@
|
|
|
70
82
|
return document.activeElement === node;
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
// Длительность --gr-transition у узла, мс: «0.2s ease» → 200, «150ms» → 150,
|
|
86
|
+
// пусто или 0s → 0. Одно чтение вычисленного стиля при закрытии.
|
|
87
|
+
function duration(node) {
|
|
88
|
+
var value = '';
|
|
89
|
+
|
|
90
|
+
try { value = getComputedStyle(node).getPropertyValue('--gr-transition'); } catch (e) { /* без CSSOM */ }
|
|
91
|
+
|
|
92
|
+
var m = /(\d*\.?\d+)(m?s)/.exec(value);
|
|
93
|
+
|
|
94
|
+
return m ? parseFloat(m[1]) * (m[2] === 's' ? 1000 : 1) : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
G.defineWidget('dropdown', { needs: ['anchor'] }, function (el, opts) {
|
|
74
98
|
var o = G.merge(DEFAULTS, opts);
|
|
75
99
|
var attrs = G.recorder();
|
|
@@ -86,6 +110,7 @@
|
|
|
86
110
|
var opened = false; // для popover и css: <details> знает сам
|
|
87
111
|
var viaKeyboard = false; // открыто с клавиатуры — фокус уходит в меню
|
|
88
112
|
var timer = null;
|
|
113
|
+
var closing = null; // таймаут состояния closing у <details>
|
|
89
114
|
|
|
90
115
|
// --- Состояние ------------------------------------------------------------
|
|
91
116
|
|
|
@@ -111,6 +136,7 @@
|
|
|
111
136
|
|
|
112
137
|
function sync(now) {
|
|
113
138
|
if (kind !== 'details') opened = now;
|
|
139
|
+
if (!now) settleClosing();
|
|
114
140
|
|
|
115
141
|
setAttr(trigger, 'aria-expanded', now ? 'true' : 'false');
|
|
116
142
|
setAttr(el, 'data-gr-state', now ? 'open' : 'ready');
|
|
@@ -121,6 +147,59 @@
|
|
|
121
147
|
viaKeyboard = false;
|
|
122
148
|
}
|
|
123
149
|
|
|
150
|
+
// --- Закрытие <details> с состоянием closing ------------------------------
|
|
151
|
+
|
|
152
|
+
// open снимается не сразу: обёртка получает data-gr-state="closing",
|
|
153
|
+
// панель уходит переходом темы; конец — transitionend/animationend самой
|
|
154
|
+
// панели (не потомка) или таймаут. Затем toggle → sync(false) → ready.
|
|
155
|
+
function closeDetails() {
|
|
156
|
+
if (closing) return;
|
|
157
|
+
|
|
158
|
+
var wait = duration(panel);
|
|
159
|
+
|
|
160
|
+
if (!wait) {
|
|
161
|
+
el.open = false;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
closing = setTimeout(finishClosing, wait + 50);
|
|
166
|
+
panel.addEventListener('transitionend', onEnd);
|
|
167
|
+
panel.addEventListener('animationend', onEnd);
|
|
168
|
+
setAttr(el, 'data-gr-state', 'closing');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onEnd(event) {
|
|
172
|
+
if (event.target === panel) finishClosing();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function settleClosing() {
|
|
176
|
+
if (!closing) return;
|
|
177
|
+
|
|
178
|
+
clearTimeout(closing);
|
|
179
|
+
closing = null;
|
|
180
|
+
panel.removeEventListener('transitionend', onEnd);
|
|
181
|
+
panel.removeEventListener('animationend', onEnd);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function finishClosing() {
|
|
185
|
+
if (!closing) return;
|
|
186
|
+
|
|
187
|
+
settleClosing();
|
|
188
|
+
el.open = false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Щелчок (и Enter/Space) по кнопке открытого <details>: платформа сняла бы
|
|
192
|
+
// open в тот же кадр — перехватывается и идёт через closing. Щелчок
|
|
193
|
+
// по закрывающемуся — передумали: панель остаётся.
|
|
194
|
+
function onTriggerClick(event) {
|
|
195
|
+
if (kind !== 'details' || !el.open) return;
|
|
196
|
+
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
|
|
199
|
+
if (closing) open();
|
|
200
|
+
else close();
|
|
201
|
+
}
|
|
202
|
+
|
|
124
203
|
// beforetoggle приходит синхронно, до показа: панель ещё без размера,
|
|
125
204
|
// но для «снизу, по началу строки» размер и не нужен, а без этой
|
|
126
205
|
// предварительной позиции она на один кадр вспыхивала бы в статическом
|
|
@@ -137,6 +216,14 @@
|
|
|
137
216
|
|
|
138
217
|
function open() {
|
|
139
218
|
cancelTimer();
|
|
219
|
+
|
|
220
|
+
// Открытие во время закрытия отменяет его: панель остаётся на месте.
|
|
221
|
+
if (closing) {
|
|
222
|
+
settleClosing();
|
|
223
|
+
setAttr(el, 'data-gr-state', 'open');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
140
227
|
if (isOpen()) return;
|
|
141
228
|
|
|
142
229
|
if (kind === 'details') el.open = true;
|
|
@@ -149,7 +236,7 @@
|
|
|
149
236
|
cancelTimer();
|
|
150
237
|
if (!isOpen()) return;
|
|
151
238
|
|
|
152
|
-
if (kind === 'details')
|
|
239
|
+
if (kind === 'details') closeDetails();
|
|
153
240
|
else if (kind === 'popover') {
|
|
154
241
|
try { panel.hidePopover(); } catch (e) { sync(false); }
|
|
155
242
|
} else sync(false);
|
|
@@ -286,6 +373,7 @@
|
|
|
286
373
|
|
|
287
374
|
function destroy() {
|
|
288
375
|
cancelTimer();
|
|
376
|
+
finishClosing();
|
|
289
377
|
events.removeAll();
|
|
290
378
|
if (kind === 'popover' && G.anchor) G.anchor.clear(panel);
|
|
291
379
|
attrs.restore();
|
|
@@ -298,8 +386,10 @@
|
|
|
298
386
|
|
|
299
387
|
roles();
|
|
300
388
|
|
|
301
|
-
if (kind === 'details')
|
|
302
|
-
|
|
389
|
+
if (kind === 'details') {
|
|
390
|
+
listen(el, 'toggle', function () { sync(!!el.open); });
|
|
391
|
+
listen(trigger, 'click', onTriggerClick);
|
|
392
|
+
} else if (kind === 'popover') {
|
|
303
393
|
listen(panel, 'beforetoggle', onBeforeToggle);
|
|
304
394
|
listen(panel, 'toggle', function (event) { sync(event.newState === 'open'); });
|
|
305
395
|
listen(document, 'scroll', onReflow, true);
|