griffincss-ui 0.27.0 → 0.28.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/griffincss-ui.js +1 -1
- package/dist/griffincss-ui.js.map +1 -1
- package/dist/griffinjs-combobox.js +1 -1
- package/dist/griffinjs-combobox.js.map +1 -1
- package/dist/griffinjs-core.js +1 -1
- package/dist/griffinjs-core.js.map +1 -1
- package/dist/griffinjs-counter.js +1 -1
- package/dist/griffinjs-counter.js.map +1 -1
- package/dist/griffinjs-datetime.js +1 -1
- package/dist/griffinjs-datetime.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.js +1 -1
- package/dist/griffinjs-fields.js.map +1 -1
- package/dist/griffinjs-file.js +1 -1
- package/dist/griffinjs-file.js.map +1 -1
- package/dist/griffinjs-lightbox.js +1 -1
- package/dist/griffinjs-lightbox.js.map +1 -1
- package/dist/griffinjs-loader.js +1 -1
- package/dist/griffinjs-loader.js.map +1 -1
- package/dist/griffinjs-mask.js +1 -1
- package/dist/griffinjs-mask.js.map +1 -1
- package/dist/griffinjs-megamenu.js +1 -1
- package/dist/griffinjs-megamenu.js.map +1 -1
- package/dist/griffinjs-otp.js +1 -1
- package/dist/griffinjs-otp.js.map +1 -1
- package/dist/griffinjs-phone.js +1 -1
- package/dist/griffinjs-phone.js.map +1 -1
- package/dist/griffinjs-range.js +1 -1
- package/dist/griffinjs-range.js.map +1 -1
- package/dist/griffinjs-rating.js +1 -1
- package/dist/griffinjs-rating.js.map +1 -1
- package/dist/griffinjs-scroll.js +1 -1
- package/dist/griffinjs-scroll.js.map +1 -1
- package/dist/griffinjs-slider.js +1 -1
- package/dist/griffinjs-slider.js.map +1 -1
- package/dist/griffinjs-sortable.js +1 -1
- package/dist/griffinjs-sortable.js.map +1 -1
- package/dist/griffinjs-tooltip.js +1 -1
- package/dist/griffinjs-tooltip.js.map +1 -1
- package/dist/griffinjs-track.js +1 -1
- package/dist/griffinjs-track.js.map +1 -1
- package/dist/griffinjs-validate.js +1 -1
- package/dist/griffinjs-validate.js.map +1 -1
- package/dist/griffinjs.js +1 -1
- package/dist/griffinjs.js.map +1 -1
- package/package.json +2 -2
- package/src/griffincss-ui.js +17 -9
- package/src/griffinjs/core/griffinjs-core.js +18 -11
- package/src/griffinjs/core/loader.js +6 -3
- package/src/griffinjs/core/options.js +1 -1
- package/src/griffinjs/core/track.js +1 -1
- package/src/griffinjs/engines/scroll.js +14 -0
- package/src/griffinjs/fields/counter.js +4 -2
- package/src/griffinjs/fields/datetime.js +5 -4
- package/src/griffinjs/fields/file.js +6 -2
- package/src/griffinjs/fields/mask.js +1 -1
- package/src/griffinjs/fields/otp.js +1 -1
- package/src/griffinjs/fields/phone.js +3 -3
- package/src/griffinjs/fields/rating.js +1 -1
- package/src/griffinjs/fields/validate.js +7 -4
- package/src/griffinjs/widgets/combobox.js +52 -7
- package/src/griffinjs/widgets/dialog.js +58 -13
- package/src/griffinjs/widgets/dropdown.js +1 -1
- package/src/griffinjs/widgets/lightbox.js +108 -19
- package/src/griffinjs/widgets/megamenu.js +45 -9
- package/src/griffinjs/widgets/range.js +6 -3
- package/src/griffinjs/widgets/slider.js +1 -1
- package/src/griffinjs/widgets/sortable.js +1 -1
- package/src/griffinjs/widgets/tooltip.js +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* GriffinJS — Core v0.
|
|
2
|
+
* GriffinJS — Core v0.28.0
|
|
3
3
|
* Опциональный слой виджетов с состоянием поверх Griffincss: слайдер,
|
|
4
4
|
* галерея, лайтбокс, параллакс, мегаменю. Подключается одной строкой
|
|
5
5
|
* и одним файлом; страница без него — законное и рабочее состояние.
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
})(function () {
|
|
46
46
|
'use strict';
|
|
47
47
|
|
|
48
|
-
var VERSION = '0.
|
|
48
|
+
var VERSION = '0.28.0';
|
|
49
49
|
|
|
50
50
|
// Реестры. Модуль зовёт defineEngine/defineWidget; ядро — единственное
|
|
51
51
|
// место, которое знает, как их применить.
|
|
@@ -84,13 +84,13 @@
|
|
|
84
84
|
if (typeof opts === 'function') { factory = opts; opts = null; }
|
|
85
85
|
|
|
86
86
|
if (typeof name !== 'string' || !name) {
|
|
87
|
-
throw new Error('GriffinJS: ' + kind + '
|
|
87
|
+
throw new Error('GriffinJS: ' + kind + ' without a name');
|
|
88
88
|
}
|
|
89
89
|
if (typeof factory !== 'function') {
|
|
90
|
-
throw new Error('GriffinJS: ' + kind + '
|
|
90
|
+
throw new Error('GriffinJS: ' + kind + ' "' + name + '" is not a function');
|
|
91
91
|
}
|
|
92
92
|
if (registry[name]) {
|
|
93
|
-
throw new Error('GriffinJS: ' + kind + '
|
|
93
|
+
throw new Error('GriffinJS: ' + kind + ' "' + name + '" is already registered');
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (opts && opts.needs) needs(name, opts.needs);
|
|
@@ -101,11 +101,11 @@
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function defineEngine(name, opts, factory) {
|
|
104
|
-
return define(engines, '
|
|
104
|
+
return define(engines, 'engine', name, opts, factory);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function defineWidget(name, opts, factory) {
|
|
108
|
-
return define(widgets, '
|
|
108
|
+
return define(widgets, 'widget', name, opts, factory);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Проверка сборки на старте: модуль подключён, а часть, которую он берёт
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
// Зависимостью бывает и часть G (track, anchor), и целый модуль:
|
|
125
125
|
// галерея строится поверх слайдера.
|
|
126
126
|
if (!api[dep] && !widgets[dep] && !engines[dep]) {
|
|
127
|
-
warn('
|
|
127
|
+
warn('"' + names[i] + '" does not work without griffinjs-' + dep + '.js');
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
// два слайдера на одной дорожке — ошибка автора, а не два состояния.
|
|
229
229
|
function mount(el, name, opts) {
|
|
230
230
|
if (!el || !widgets[name]) {
|
|
231
|
-
warn('
|
|
231
|
+
warn('widget "' + name + '" is not registered');
|
|
232
232
|
return null;
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
record.instance = widgets[name](el, opts, api) || {};
|
|
245
245
|
} catch (e) {
|
|
246
246
|
mounted.splice(mounted.indexOf(record), 1);
|
|
247
|
-
warn('
|
|
247
|
+
warn('widget "' + name + '" failed to mount: ' + (e && e.message));
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -369,7 +369,14 @@
|
|
|
369
369
|
function autoStart() {
|
|
370
370
|
var script = null;
|
|
371
371
|
|
|
372
|
-
|
|
372
|
+
// Флаги — только у настоящего тега: именованный элемент перекрывает
|
|
373
|
+
// свойство документа — <img name="currentScript"> даёт картинку,
|
|
374
|
+
// <iframe name> — окно фрейма (у чужого origin чтение бросает).
|
|
375
|
+
try {
|
|
376
|
+
var tag = document.currentScript;
|
|
377
|
+
|
|
378
|
+
if (tag && tag.tagName === 'SCRIPT') script = tag;
|
|
379
|
+
} catch (e) { /* currentScript недоступен */ }
|
|
373
380
|
|
|
374
381
|
if (script && script.getAttribute('data-auto') === 'false') return;
|
|
375
382
|
|
|
@@ -37,10 +37,13 @@
|
|
|
37
37
|
var loading = null; // null — не запрашивали; pending | done | failed
|
|
38
38
|
var warned = {}; // поле → без config уже сказано
|
|
39
39
|
|
|
40
|
+
// Только настоящий тег: именованный элемент перекрывает свойство
|
|
41
|
+
// документа, и <img name="currentScript" data-fields> дал бы адрес
|
|
42
|
+
// бандла из чужой разметки.
|
|
40
43
|
try {
|
|
41
44
|
var script = document.currentScript;
|
|
42
45
|
|
|
43
|
-
if (script) {
|
|
46
|
+
if (script && script.tagName === 'SCRIPT') {
|
|
44
47
|
nonce = script.nonce || '';
|
|
45
48
|
|
|
46
49
|
var src = script.getAttribute('data-fields');
|
|
@@ -62,7 +65,7 @@
|
|
|
62
65
|
if (config.fields && config.fields.src) return request();
|
|
63
66
|
|
|
64
67
|
warned[name] = true;
|
|
65
|
-
G.warn('
|
|
68
|
+
G.warn('field "' + name + '" is not loaded: include griffinjs-fields.js or set GriffinJS.config.fields');
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -85,7 +88,7 @@
|
|
|
85
88
|
function settle(event) {
|
|
86
89
|
if (event.type === 'error') {
|
|
87
90
|
ok = false;
|
|
88
|
-
G.warn('
|
|
91
|
+
G.warn('fields bundle failed to load: ' + event.target.src + '; fields stay without the script');
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
if (--left) return;
|
|
@@ -542,7 +542,7 @@
|
|
|
542
542
|
var factory = G.engines[o.engine];
|
|
543
543
|
|
|
544
544
|
if (!factory) {
|
|
545
|
-
G.warn('
|
|
545
|
+
G.warn('engine "' + o.engine + '" is not registered; the track will not move');
|
|
546
546
|
} else {
|
|
547
547
|
engine = factory(api, el, o) || null;
|
|
548
548
|
}
|
|
@@ -54,8 +54,22 @@
|
|
|
54
54
|
|
|
55
55
|
// --- Клоны -----------------------------------------------------------------
|
|
56
56
|
|
|
57
|
+
// Клон — декорация на время доезда до края: медиа в нём не грузится.
|
|
58
|
+
// Плеер во фрейме-клоне грузился бы вторым (с autoplay — играл бы
|
|
59
|
+
// невидимо), <video> качал бы файл ещё раз; src снимается до вставки.
|
|
57
60
|
function makeClone(slide) {
|
|
58
61
|
var node = slide.cloneNode(true);
|
|
62
|
+
var media = node.querySelectorAll('iframe,video,audio,source');
|
|
63
|
+
|
|
64
|
+
// Шаг −1 — сам слайд: видео-слайдер ставит слайдом <video>;
|
|
65
|
+
// картинка-слайд остаётся с src — она и видна на доезде.
|
|
66
|
+
for (var i = -1; i < media.length; i++) {
|
|
67
|
+
var m = i < 0 ? node : media[i];
|
|
68
|
+
|
|
69
|
+
if (m.tagName === 'IMG') continue;
|
|
70
|
+
m.removeAttribute('src');
|
|
71
|
+
m.removeAttribute('srcdoc');
|
|
72
|
+
}
|
|
59
73
|
|
|
60
74
|
node.setAttribute(CLONE, '');
|
|
61
75
|
node.setAttribute('aria-hidden', 'true');
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
var attrs = G.recorder();
|
|
33
33
|
var events = G.listeners();
|
|
34
34
|
|
|
35
|
-
if (!(max > 0)) throw new Error('
|
|
35
|
+
if (!(max > 0)) throw new Error('the field has no maxlength: nothing to count');
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Цель — в своей форме (без формы — в документе): out приходит
|
|
38
|
+
// и из данных, и out: main не должен сделать подписью <main>.
|
|
39
|
+
var out = o.out ? (G.closest(el, 'form') || document).querySelector(o.out) : null;
|
|
38
40
|
var created = false;
|
|
39
41
|
|
|
40
42
|
if (!out) {
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
var native = el.getAttribute('type');
|
|
236
236
|
var mode = MODES[native];
|
|
237
237
|
|
|
238
|
-
if (!mode) throw new Error('
|
|
238
|
+
if (!mode) throw new Error('needs type="date", "time" or "datetime-local"');
|
|
239
239
|
|
|
240
240
|
var attrs = G.recorder();
|
|
241
241
|
var events = G.listeners(); // на всё время жизни
|
|
@@ -792,12 +792,13 @@
|
|
|
792
792
|
|
|
793
793
|
// Сосед по паре: уже поднятый — как есть, иначе поднимается здесь;
|
|
794
794
|
// сканер на нём потом найдёт живой экземпляр.
|
|
795
|
+
// Сосед — поле с data-gr-datetime своей формы (без формы — документа).
|
|
795
796
|
if (o.to && mode !== 'time') {
|
|
796
|
-
var target = document.querySelector(o.to);
|
|
797
|
-
var other = target && G.mount(target, 'datetime');
|
|
797
|
+
var target = (G.closest(el, 'form') || document).querySelector(o.to);
|
|
798
|
+
var other = target && target.hasAttribute('data-gr-datetime') && G.mount(target, 'datetime');
|
|
798
799
|
|
|
799
800
|
if (other) { peer = other; role = 'from'; other.pair(api); push(); }
|
|
800
|
-
else G.warn('datetime:
|
|
801
|
+
else G.warn('datetime: the "to" field is not found: ' + o.to);
|
|
801
802
|
}
|
|
802
803
|
|
|
803
804
|
return api;
|
|
@@ -77,12 +77,16 @@
|
|
|
77
77
|
G.defineWidget('file', function (el, opts) {
|
|
78
78
|
var o = G.merge(DEFAULTS, opts);
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
// Только на самом поле: на обёртке change любого потомка чистил бы цель.
|
|
81
|
+
if (String(el.tagName).toLowerCase() !== 'input' || String(el.getAttribute('type')).toLowerCase() !== 'file') throw new Error('needs <input type="file">');
|
|
82
|
+
|
|
83
|
+
if (typeof DataTransfer !== 'function') throw new Error('without DataTransfer a single file cannot be removed from the selection');
|
|
81
84
|
|
|
82
85
|
var attrs = G.recorder();
|
|
83
86
|
var events = G.listeners();
|
|
84
87
|
var locale = lang();
|
|
85
|
-
|
|
88
|
+
// Цель — в своей форме (без формы — в документе).
|
|
89
|
+
var list = o.list ? (G.closest(el, 'form') || document).querySelector(o.list) : null;
|
|
86
90
|
var created = false;
|
|
87
91
|
var known = [];
|
|
88
92
|
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
// разобрался бы как пара «00: 00», поэтому без ключа берётся строка как есть.
|
|
164
164
|
var format = o.format ? String(o.format) : (el.getAttribute('data-gr-mask') || '').trim();
|
|
165
165
|
|
|
166
|
-
if (!format) throw new Error('
|
|
166
|
+
if (!format) throw new Error('the mask has no format');
|
|
167
167
|
|
|
168
168
|
var tokens;
|
|
169
169
|
var last = null; // последнее записанное значение
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
G.defineWidget('otp', function (el) {
|
|
36
36
|
var cells = el.querySelectorAll('input');
|
|
37
37
|
|
|
38
|
-
if (!cells.length) throw new Error('
|
|
38
|
+
if (!cells.length) throw new Error('no <input> cells inside');
|
|
39
39
|
|
|
40
40
|
var attrs = G.recorder();
|
|
41
41
|
var events = G.listeners();
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
var input = el.querySelector('input');
|
|
94
94
|
var locale = lang();
|
|
95
95
|
|
|
96
|
-
if (!input) throw new Error('
|
|
96
|
+
if (!input) throw new Error('the phone field has no <input>');
|
|
97
97
|
|
|
98
98
|
var created = []; // позиции, добавленные из таблицы
|
|
99
99
|
var texts = []; // [позиция, подпись автора] — для destroy
|
|
@@ -142,11 +142,11 @@
|
|
|
142
142
|
// Без country первой стоит первая по алфавиту языка страницы —
|
|
143
143
|
// на русской это Австралия, +61. Угадывать страну за автора виджет
|
|
144
144
|
// не берётся: ошибся бы тише, чем сейчас. Но и молчать не должен.
|
|
145
|
-
if (!o.country) G.warn('
|
|
145
|
+
if (!o.country) G.warn('phone: no country set, the first one alphabetically is used; set country');
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
if (!options().length) {
|
|
149
|
-
G.warn('
|
|
149
|
+
G.warn('phone: the code list is empty; list the <option>s yourself or include griffinjs-countries.js');
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// Подписи: флаг + имя в языке страницы + код. Только там, где есть ISO
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
var display = el.querySelector('.gr-rating');
|
|
38
38
|
var radios = el.querySelectorAll('input[type="radio"]');
|
|
39
39
|
|
|
40
|
-
if (!display || !radios.length) throw new Error('
|
|
40
|
+
if (!display || !radios.length) throw new Error('needs .gr-rating and <input type="radio"> inside');
|
|
41
41
|
|
|
42
42
|
var attrs = G.recorder();
|
|
43
43
|
var events = G.listeners();
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
G.defineWidget('validate', function (el, opts) {
|
|
48
|
-
if (String(el.tagName).toLowerCase() !== 'form') throw new Error('
|
|
48
|
+
if (String(el.tagName).toLowerCase() !== 'form') throw new Error('needs a <form>');
|
|
49
49
|
|
|
50
50
|
var o = G.merge(DEFAULTS, opts);
|
|
51
51
|
var attrs = G.recorder();
|
|
52
52
|
var events = G.listeners();
|
|
53
|
-
|
|
53
|
+
// Сводка — внутри своей формы: summary: main спрятал бы <main>.
|
|
54
|
+
var box = o.summary ? el.querySelector(o.summary) : null;
|
|
54
55
|
var created = false;
|
|
55
56
|
var marked = []; // поля, которым поставлен aria-invalid
|
|
56
57
|
|
|
@@ -72,9 +73,11 @@
|
|
|
72
73
|
return control.validity ? control.validity.valid === false : false;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
// Подпись поля: <label>, иначе aria-label, иначе name.
|
|
76
|
+
// Подпись поля: <label>, иначе aria-label, иначе name. Только
|
|
77
|
+
// control.labels: селектор label[for="…"] из id с кавычкой бросал
|
|
78
|
+
// до preventDefault, и форма уходила без проверки.
|
|
76
79
|
function labelOf(control) {
|
|
77
|
-
var label =
|
|
80
|
+
var label = control.labels && control.labels[0];
|
|
78
81
|
var text = label ? String(label.textContent || '').trim() : '';
|
|
79
82
|
|
|
80
83
|
return text || control.getAttribute('aria-label') || control.getAttribute('name') || '';
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
* Источник — адрес с {q} (ответ — JSON: массив строк или объектов
|
|
9
9
|
* {value, label, href, group, id}, либо {items: […]}) или функция
|
|
10
10
|
* через JS: GriffinJS.mount(el, 'combobox', { source: (q, signal) => Promise }).
|
|
11
|
+
* С флагом navigate выбор позиции переходит по её href — только http(s).
|
|
11
12
|
* Рендер позиции подменяется render(item) → узел.
|
|
12
13
|
*
|
|
14
|
+
* Набранное уходит источнику с каждым запросом, и CORS этого не держит —
|
|
15
|
+
* он закрывает только чтение ответа. Поэтому адрес из разметки — только
|
|
16
|
+
* http(s) своего origin; другой сайт разрешает JS темы:
|
|
17
|
+
* GriffinJS.config.sources.push('https://api.example.ru') (origin, по
|
|
18
|
+
* умолчанию список пуст) — или функция source. Редирект не выполняется:
|
|
19
|
+
* по 302 запрос с набранным ушёл бы дальше.
|
|
20
|
+
*
|
|
13
21
|
* База без JS — само поле: форма поиска отправляется Enter, как всегда.
|
|
14
22
|
* Скрипт добавляет список под полем и то, что описано в WAI-ARIA combobox:
|
|
15
23
|
* role=combobox с aria-autocomplete=list, listbox с option,
|
|
@@ -66,6 +74,26 @@
|
|
|
66
74
|
};
|
|
67
75
|
|
|
68
76
|
var seq = 0;
|
|
77
|
+
var config = G.config;
|
|
78
|
+
|
|
79
|
+
if (!config.sources) config.sources = [];
|
|
80
|
+
|
|
81
|
+
// Адрес запроса — только http(s) своего origin или origin из списка
|
|
82
|
+
// (записи сводятся к origin: путь и слеш в конце не мешают). Список
|
|
83
|
+
// читается при каждом запросе — присваивание работает, как push.
|
|
84
|
+
function allowed(src) {
|
|
85
|
+
var url = null;
|
|
86
|
+
|
|
87
|
+
try { url = new URL(src, document.baseURI); } catch (e) { /* не адрес */ }
|
|
88
|
+
|
|
89
|
+
if (url && /^https?:$/.test(url.protocol) && (url.origin === location.origin || [].concat(config.sources).some(function (entry) {
|
|
90
|
+
try { return new URL(entry).origin === url.origin; } catch (e) { return false; }
|
|
91
|
+
}))) return url;
|
|
92
|
+
|
|
93
|
+
G.warn('combobox: source refused: ' + src + ' (other origins: GriffinJS.config.sources)');
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
69
97
|
|
|
70
98
|
function normalize(raw) {
|
|
71
99
|
var items = raw && raw.items ? raw.items : raw;
|
|
@@ -140,12 +168,12 @@
|
|
|
140
168
|
|
|
141
169
|
var input = el.querySelector('input');
|
|
142
170
|
|
|
143
|
-
if (!input) throw new Error('
|
|
171
|
+
if (!input) throw new Error('the combobox has no input');
|
|
144
172
|
|
|
145
173
|
var choice = o.multiple ? el.querySelector('select[multiple]') : null;
|
|
146
174
|
|
|
147
|
-
if (o.multiple && !choice) throw new Error('
|
|
148
|
-
if (o.free && !choice) { G.warn('
|
|
175
|
+
if (o.multiple && !choice) throw new Error('multiple needs a <select multiple> inside');
|
|
176
|
+
if (o.free && !choice) { G.warn('combobox: free makes no sense without multiple; flag ignored'); o.free = false; }
|
|
149
177
|
|
|
150
178
|
var list = el.querySelector('[role="listbox"]');
|
|
151
179
|
var created = false;
|
|
@@ -271,12 +299,16 @@
|
|
|
271
299
|
if (typeof o.source === 'function') {
|
|
272
300
|
promise = Promise.resolve(o.source(query, signal));
|
|
273
301
|
} else if (o.src && typeof fetch === 'function') {
|
|
274
|
-
|
|
302
|
+
var url = allowed(o.src.replace('{q}', encodeURIComponent(query)));
|
|
303
|
+
|
|
304
|
+
if (!url) return;
|
|
305
|
+
|
|
306
|
+
promise = fetch(url.href, { signal: signal, redirect: 'error' }).then(function (response) {
|
|
275
307
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
276
308
|
return response.json();
|
|
277
309
|
});
|
|
278
310
|
} else {
|
|
279
|
-
G.warn('
|
|
311
|
+
G.warn('combobox: no source (src or source)');
|
|
280
312
|
return;
|
|
281
313
|
}
|
|
282
314
|
|
|
@@ -292,7 +324,7 @@
|
|
|
292
324
|
render(query, found);
|
|
293
325
|
}, function (error) {
|
|
294
326
|
if (error && error.name === 'AbortError') return;
|
|
295
|
-
G.warn('
|
|
327
|
+
G.warn('combobox: request failed: ' + (error && error.message));
|
|
296
328
|
});
|
|
297
329
|
}
|
|
298
330
|
|
|
@@ -440,11 +472,24 @@
|
|
|
440
472
|
close();
|
|
441
473
|
G.emit('griffin:select', input, { item: item, index: index, input: input });
|
|
442
474
|
|
|
443
|
-
if (o.navigate && item.href && typeof location !== 'undefined')
|
|
475
|
+
if (o.navigate && item.href && typeof location !== 'undefined') navigate(String(item.href));
|
|
444
476
|
|
|
445
477
|
return api;
|
|
446
478
|
}
|
|
447
479
|
|
|
480
|
+
// Переход — только по http(s): href приходит из ответа источника,
|
|
481
|
+
// и javascript: исполнился бы в origin страницы. Выбор при отказе
|
|
482
|
+
// остаётся — значение в поле, событие ушло. Адрес — строка, приведённая
|
|
483
|
+
// один раз: она проверяется, она же уходит в location.
|
|
484
|
+
function navigate(href) {
|
|
485
|
+
var url = null;
|
|
486
|
+
|
|
487
|
+
try { url = new URL(href, document.baseURI); } catch (e) { /* не адрес */ }
|
|
488
|
+
|
|
489
|
+
if (url && /^https?:$/.test(url.protocol)) location.href = href;
|
|
490
|
+
else G.warn('combobox: navigation refused: ' + href);
|
|
491
|
+
}
|
|
492
|
+
|
|
448
493
|
// --- События --------------------------------------------------------------
|
|
449
494
|
|
|
450
495
|
function onInput() { query(); }
|
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* * остановка медиа при закрытии: <video>/<audio> на паузу, <iframe>
|
|
20
20
|
* разгружается и возвращается при следующем открытии;
|
|
21
21
|
* * ajax-содержимое: data-gr-src грузится один раз в [data-gr-content]
|
|
22
|
-
* (или в само окно), состояния loading → open | error
|
|
22
|
+
* (или в само окно), состояния loading → open | error. Только ответ
|
|
23
|
+
* своего origin с типом text/html: чужой адрес, data: и прочие
|
|
24
|
+
* схемы — error без запроса;
|
|
23
25
|
* * hash: открытие пишет #id в историю, «Назад» закрывает окно,
|
|
24
|
-
* а страница, открытая с #id, показывает окно
|
|
26
|
+
* а страница, открытая с #id, показывает окно сразу — закрытие
|
|
27
|
+
* такого окна снимает #id из адреса на месте, а не шагом назад;
|
|
25
28
|
* * рост окна (Этап 48d) — data-gr-dialog="grow": ResizeObserver
|
|
26
29
|
* на окне, и смена высоты больше 24 px идёт анимацией от прежней
|
|
27
30
|
* к новой (element.animate по block-size — не спорит с transition
|
|
@@ -80,6 +83,44 @@
|
|
|
80
83
|
return (value || '').replace(/^#/, '');
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// Фрагмент — только ответ своего origin с типом text/html: разметку
|
|
87
|
+
// из innerHTML браузер оживляет, и <img onerror> чужого ответа исполнился
|
|
88
|
+
// бы в origin страницы. Адрес — http(s) своего origin (у data: origin —
|
|
89
|
+
// строка 'null'); редирект на чужой отклоняет mode: 'same-origin'; тип
|
|
90
|
+
// ответа отсекает свой JSON или текст, отражающий запрос. Тип — целиком:
|
|
91
|
+
// text/htmlx — не HTML, а склейку двух заголовков («text/html,
|
|
92
|
+
// application/json») браузер читает по последнему типу.
|
|
93
|
+
function fragment(src) {
|
|
94
|
+
var url = null;
|
|
95
|
+
|
|
96
|
+
try { url = new URL(src, document.baseURI); } catch (e) { /* не адрес */ }
|
|
97
|
+
|
|
98
|
+
if (!url || !/^https?:$/.test(url.protocol) || url.origin !== location.origin) {
|
|
99
|
+
return Promise.reject(new Error('foreign origin'));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return fetch(url.href, { mode: 'same-origin' }).then(function (response) {
|
|
103
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
104
|
+
if (!/^text\/html\s*(;[^,]*)?$/i.test(response.headers.get('content-type'))) throw new Error('not text/html');
|
|
105
|
+
|
|
106
|
+
return response.text();
|
|
107
|
+
}).then(function (html) {
|
|
108
|
+
// Кусок разметки — без meta, base и link: refresh увёл бы страницу
|
|
109
|
+
// без щелчка, base сменил бы базу адресов всей страницы, а в куске
|
|
110
|
+
// разметки они — ошибка или подлог. В <template> они не срабатывают
|
|
111
|
+
// и снимаются до вставки; <script> куска не исполняется и так.
|
|
112
|
+
var tpl = document.createElement('template');
|
|
113
|
+
|
|
114
|
+
tpl.innerHTML = html;
|
|
115
|
+
|
|
116
|
+
var bad = tpl.content.querySelectorAll('meta, base, link');
|
|
117
|
+
|
|
118
|
+
for (var i = 0; i < bad.length; i++) bad[i].remove();
|
|
119
|
+
|
|
120
|
+
return tpl.content;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
// Прокручиваемый предок внутри окна: жест в теле окна с прокруткой —
|
|
84
125
|
// законный, всё остальное двигало бы страницу.
|
|
85
126
|
function scrollable(node, root) {
|
|
@@ -135,6 +176,7 @@
|
|
|
135
176
|
var loaded = false;
|
|
136
177
|
var frames = []; // [{el, src}] — разгруженные iframe
|
|
137
178
|
var byHistory = false; // закрыто кнопкой «Назад»: back() не зовём
|
|
179
|
+
var pushed = false; // #id в адресе записало само окно
|
|
138
180
|
var record = { el: el, o: o };
|
|
139
181
|
var grow = null; // ResizeObserver — только по grow
|
|
140
182
|
var known = 0; // высота окна по последнему наблюдению
|
|
@@ -181,7 +223,7 @@
|
|
|
181
223
|
loaded = true;
|
|
182
224
|
|
|
183
225
|
if (typeof fetch !== 'function') {
|
|
184
|
-
G.warn('
|
|
226
|
+
G.warn('dialog: fetch is unavailable, ' + src + ' not loaded');
|
|
185
227
|
setAttr(el, 'data-gr-state', 'error');
|
|
186
228
|
return;
|
|
187
229
|
}
|
|
@@ -190,15 +232,11 @@
|
|
|
190
232
|
|
|
191
233
|
setAttr(el, 'data-gr-state', 'loading');
|
|
192
234
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return response.text();
|
|
197
|
-
}).then(function (html) {
|
|
198
|
-
target.innerHTML = html;
|
|
235
|
+
fragment(src).then(function (html) {
|
|
236
|
+
target.replaceChildren(html);
|
|
199
237
|
setAttr(el, 'data-gr-state', isOpen() ? 'open' : 'ready');
|
|
200
238
|
}, function (error) {
|
|
201
|
-
G.warn('
|
|
239
|
+
G.warn('dialog: ' + src + ' not loaded: ' + (error && error.message));
|
|
202
240
|
setAttr(el, 'data-gr-state', 'error');
|
|
203
241
|
});
|
|
204
242
|
}
|
|
@@ -244,13 +282,19 @@
|
|
|
244
282
|
function pushHash() {
|
|
245
283
|
if (!o.hash || !id() || typeof history === 'undefined' || hashIs()) return;
|
|
246
284
|
|
|
247
|
-
try { history.pushState(null, '', '#' + id()); } catch (e) { /* file:// и прочее */ }
|
|
285
|
+
try { history.pushState(null, '', '#' + id()); pushed = true; } catch (e) { /* file:// и прочее */ }
|
|
248
286
|
}
|
|
249
287
|
|
|
288
|
+
// Свой #id закрытие убирает шагом назад по истории. #id, пришедший
|
|
289
|
+
// со ссылкой (страница открыта сразу с окном), так не убрать: «назад»
|
|
290
|
+
// увёл бы с сайта. Он снимается из адреса на месте, без новой записи.
|
|
250
291
|
function popHash() {
|
|
251
292
|
if (!o.hash || byHistory || typeof history === 'undefined' || !hashIs()) return;
|
|
252
293
|
|
|
253
|
-
try {
|
|
294
|
+
try {
|
|
295
|
+
if (pushed) history.back();
|
|
296
|
+
else history.replaceState(history.state, '', location.pathname + location.search);
|
|
297
|
+
} catch (e) { /* без истории */ }
|
|
254
298
|
}
|
|
255
299
|
|
|
256
300
|
// --- Открытие и закрытие ------------------------------------------------
|
|
@@ -291,6 +335,7 @@
|
|
|
291
335
|
relock();
|
|
292
336
|
stopMedia();
|
|
293
337
|
popHash();
|
|
338
|
+
pushed = false;
|
|
294
339
|
byHistory = false;
|
|
295
340
|
|
|
296
341
|
if (el.getAttribute('data-gr-state') !== 'loading') setAttr(el, 'data-gr-state', 'ready');
|
|
@@ -356,7 +401,7 @@
|
|
|
356
401
|
function open(target) {
|
|
357
402
|
var w = instanceOf(target);
|
|
358
403
|
|
|
359
|
-
if (!w) { G.warn('
|
|
404
|
+
if (!w) { G.warn('dialog not found: ' + target); return null; }
|
|
360
405
|
|
|
361
406
|
return w.open();
|
|
362
407
|
}
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
var trigger = el.firstElementChild;
|
|
105
105
|
var panel = trigger && trigger.nextElementSibling;
|
|
106
106
|
|
|
107
|
-
if (!trigger || !panel) throw new Error('
|
|
107
|
+
if (!trigger || !panel) throw new Error('the dropdown has no button and panel');
|
|
108
108
|
|
|
109
109
|
var kind = el.tagName === 'DETAILS' ? 'details' : panel.hasAttribute('popover') ? 'popover' : 'css';
|
|
110
110
|
var opened = false; // для popover и css: <details> знает сам
|