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.
Files changed (72) hide show
  1. package/dist/griffincss-ui.js +1 -1
  2. package/dist/griffincss-ui.js.map +1 -1
  3. package/dist/griffinjs-combobox.js +1 -1
  4. package/dist/griffinjs-combobox.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-counter.js +1 -1
  8. package/dist/griffinjs-counter.js.map +1 -1
  9. package/dist/griffinjs-datetime.js +1 -1
  10. package/dist/griffinjs-datetime.js.map +1 -1
  11. package/dist/griffinjs-dialog.js +1 -1
  12. package/dist/griffinjs-dialog.js.map +1 -1
  13. package/dist/griffinjs-dropdown.js +1 -1
  14. package/dist/griffinjs-dropdown.js.map +1 -1
  15. package/dist/griffinjs-fields.js +1 -1
  16. package/dist/griffinjs-fields.js.map +1 -1
  17. package/dist/griffinjs-file.js +1 -1
  18. package/dist/griffinjs-file.js.map +1 -1
  19. package/dist/griffinjs-lightbox.js +1 -1
  20. package/dist/griffinjs-lightbox.js.map +1 -1
  21. package/dist/griffinjs-loader.js +1 -1
  22. package/dist/griffinjs-loader.js.map +1 -1
  23. package/dist/griffinjs-mask.js +1 -1
  24. package/dist/griffinjs-mask.js.map +1 -1
  25. package/dist/griffinjs-megamenu.js +1 -1
  26. package/dist/griffinjs-megamenu.js.map +1 -1
  27. package/dist/griffinjs-otp.js +1 -1
  28. package/dist/griffinjs-otp.js.map +1 -1
  29. package/dist/griffinjs-phone.js +1 -1
  30. package/dist/griffinjs-phone.js.map +1 -1
  31. package/dist/griffinjs-range.js +1 -1
  32. package/dist/griffinjs-range.js.map +1 -1
  33. package/dist/griffinjs-rating.js +1 -1
  34. package/dist/griffinjs-rating.js.map +1 -1
  35. package/dist/griffinjs-scroll.js +1 -1
  36. package/dist/griffinjs-scroll.js.map +1 -1
  37. package/dist/griffinjs-slider.js +1 -1
  38. package/dist/griffinjs-slider.js.map +1 -1
  39. package/dist/griffinjs-sortable.js +1 -1
  40. package/dist/griffinjs-sortable.js.map +1 -1
  41. package/dist/griffinjs-tooltip.js +1 -1
  42. package/dist/griffinjs-tooltip.js.map +1 -1
  43. package/dist/griffinjs-track.js +1 -1
  44. package/dist/griffinjs-track.js.map +1 -1
  45. package/dist/griffinjs-validate.js +1 -1
  46. package/dist/griffinjs-validate.js.map +1 -1
  47. package/dist/griffinjs.js +1 -1
  48. package/dist/griffinjs.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/griffincss-ui.js +17 -9
  51. package/src/griffinjs/core/griffinjs-core.js +18 -11
  52. package/src/griffinjs/core/loader.js +6 -3
  53. package/src/griffinjs/core/options.js +1 -1
  54. package/src/griffinjs/core/track.js +1 -1
  55. package/src/griffinjs/engines/scroll.js +14 -0
  56. package/src/griffinjs/fields/counter.js +4 -2
  57. package/src/griffinjs/fields/datetime.js +5 -4
  58. package/src/griffinjs/fields/file.js +6 -2
  59. package/src/griffinjs/fields/mask.js +1 -1
  60. package/src/griffinjs/fields/otp.js +1 -1
  61. package/src/griffinjs/fields/phone.js +3 -3
  62. package/src/griffinjs/fields/rating.js +1 -1
  63. package/src/griffinjs/fields/validate.js +7 -4
  64. package/src/griffinjs/widgets/combobox.js +52 -7
  65. package/src/griffinjs/widgets/dialog.js +58 -13
  66. package/src/griffinjs/widgets/dropdown.js +1 -1
  67. package/src/griffinjs/widgets/lightbox.js +108 -19
  68. package/src/griffinjs/widgets/megamenu.js +45 -9
  69. package/src/griffinjs/widgets/range.js +6 -3
  70. package/src/griffinjs/widgets/slider.js +1 -1
  71. package/src/griffinjs/widgets/sortable.js +1 -1
  72. package/src/griffinjs/widgets/tooltip.js +1 -1
@@ -9,8 +9,23 @@
9
9
  * в один кадр без кнопок. Адрес кадра — data-gr-src, без него — href:
10
10
  * у видео это разные адреса — во фрейм встаёт только плеер, страницу ролика
11
11
  * хостинги во фрейме не показывают, а ссылка без скрипта ведёт именно на неё.
12
- * Тип — по адресу кадра (картинка, видео, iframe для плееров YouTube, Vimeo,
13
- * Rutube и VK Видео) или явно: data-gr-type="iframe".
12
+ * Тип — по адресу кадра (картинка, видео, iframe для хостов списка кадров)
13
+ * или явно: data-gr-type="iframe".
14
+ *
15
+ * Адрес кадра — http(s), относительный, data: или blob:. Любой другой кадр
16
+ * не открывает: javascript: во фрейме исполнился бы в origin страницы,
17
+ * а data-* редакторы и фильтры CMS не проверяют как ссылку. Отвергнутый
18
+ * кадр из группы выпадает; щелчок по нему остаётся браузеру.
19
+ *
20
+ * Фреймом — только адрес из списка GriffinJS.config.frames: иначе в окне
21
+ * сайта с его адресом в строке встала бы любая страница (форма входа,
22
+ * data: без сервера). Запись — хост (поддомены входят), хост с началом
23
+ * пути или 'self' — свой origin. Список по умолчанию ставит модуль, если
24
+ * сайт не задал свой; push расширяет, присваивание заменяет, читается он
25
+ * при каждом открытии. Каждый кадр — в песочнице: вкладку не уводит,
26
+ * allow-same-origin — только хостам списка (без него плееры не
27
+ * поднимаются), своему origin — никогда: его SVG со <script> получил бы
28
+ * доступ к странице.
14
29
  *
15
30
  * База без JS — ссылка на большое изображение или на страницу ролика.
16
31
  * Окно строится при открытии и уничтожается при закрытии: так видео
@@ -23,7 +38,13 @@
23
38
 
24
39
  var ATTR = 'data-gr-lightbox';
25
40
  var VIDEO = /\.(mp4|webm|ogv|mov)(\?|#|$)/i;
26
- var EMBED = /youtube\.com|youtu\.be|vimeo\.com|rutube\.ru|vkvideo\.ru|vk\.com\/video/i;
41
+ var SCHEMES = /^(https?|data|blob):$/;
42
+ // Набор — по замеру настоящих плееров: allow-presentation им не нужен,
43
+ // а WebKit на него пишет ошибку в консоль.
44
+ var SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox';
45
+ var config = G.config;
46
+
47
+ if (!config.frames) config.frames = ['self', 'youtube.com', 'youtu.be', 'vimeo.com', 'rutube.ru', 'vkvideo.ru', 'vk.com/video'];
27
48
 
28
49
  var dialog = null;
29
50
  var track = null;
@@ -31,14 +52,48 @@
31
52
  var events = G.listeners();
32
53
  var listen = events.add;
33
54
 
55
+ // Адрес по правилам браузера: регистр, пробелы по краям и табы внутри
56
+ // схемы разбор снимает. Не адрес — null.
57
+ function parse(src) {
58
+ try {
59
+ return new URL(src, document.baseURI);
60
+ } catch (e) {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ // Адрес в списке кадров: 'self' — свой origin, 'host' — хост списка,
66
+ // '' — нет. Хост и путь сверяются с началом записи: хостинг — сам хост
67
+ // или его поддомен, а не подстрока где угодно (…/rutube.ru-logo.png —
68
+ // не плеер). Свой origin — всегда 'self', даже под записью-хостом.
69
+ // Страница с диска: origin у неё и у её файлов непрозрачный ('null'),
70
+ // своё узнаётся по протоколу file:; data: и blob: кадром не бывают.
71
+ function listed(url) {
72
+ if (!url || !/^(https?|file):$/.test(url.protocol)) return '';
73
+
74
+ var own = url.protocol === 'file:' ? location.protocol === 'file:' : url.origin === location.origin;
75
+ var where = (url.hostname + url.pathname).toLowerCase();
76
+ var list = [].concat(config.frames);
77
+
78
+ for (var i = 0; i < list.length; i++) {
79
+ var entry = String(list[i]).toLowerCase();
80
+ var at = where.indexOf(entry.indexOf('/') < 0 ? entry + '/' : entry);
81
+
82
+ if (entry === 'self' ? own : !at || (at > 0 && at < url.hostname.length && where.charAt(at - 1) === '.')) return own ? 'self' : 'host';
83
+ }
84
+
85
+ return '';
86
+ }
87
+
88
+ // Без data-gr-type фреймом угадываются хосты списка, но не свой сайт:
89
+ // свои адреса — картинки и видео.
34
90
  function typeOf(node, src) {
35
91
  var explicit = node && node.getAttribute && node.getAttribute('data-gr-type');
36
92
 
37
93
  if (explicit) return explicit;
38
94
  if (VIDEO.test(src)) return 'video';
39
- if (EMBED.test(src)) return 'iframe';
40
95
 
41
- return 'image';
96
+ return listed(parse(src)) === 'host' ? 'iframe' : 'image';
42
97
  }
43
98
 
44
99
  // Элемент разметки → описание кадра. data-gr-src старше href: у видео
@@ -55,15 +110,19 @@
55
110
  };
56
111
  }
57
112
 
113
+ // Имя группы сравнивается, а не склеивается в селектор: кавычка в нём
114
+ // роняла бы querySelectorAll, подобранное — собирала бы чужие ссылки.
58
115
  function groupOf(node) {
59
116
  var name = node.getAttribute(ATTR);
60
117
 
61
118
  if (!name) return [node];
62
119
 
63
- var all = document.querySelectorAll('[' + ATTR + '="' + name + '"]');
120
+ var all = document.querySelectorAll('[' + ATTR + ']');
64
121
  var out = [];
65
122
 
66
- for (var i = 0; i < all.length; i++) out.push(all[i]);
123
+ for (var i = 0; i < all.length; i++) {
124
+ if (all[i].getAttribute(ATTR) === name) out.push(all[i]);
125
+ }
67
126
 
68
127
  return out;
69
128
  }
@@ -86,7 +145,7 @@
86
145
  }
87
146
 
88
147
  if (item.type === 'iframe') {
89
- return element('iframe', null, { src: item.src, allow: 'autoplay; fullscreen; picture-in-picture', allowfullscreen: '', title: item.caption || item.alt || 'Видео' });
148
+ return element('iframe', null, { sandbox: SANDBOX + (item.same ? ' allow-same-origin' : ''), src: item.src, allow: 'autoplay; fullscreen; picture-in-picture', allowfullscreen: '', title: item.caption || item.alt || 'Видео' });
90
149
  }
91
150
 
92
151
  return element('img', null, { src: item.src, alt: item.alt });
@@ -97,15 +156,43 @@
97
156
  // --- Открытие ----------------------------------------------------------------
98
157
 
99
158
  function open(items, index, opts) {
159
+ var list = [];
160
+ var at = index || 0; // номер открываемого в исходной группе
161
+
162
+ // Кадр с отвергнутым адресом выпадает из группы; отвергнут
163
+ // открываемый — окна нет. Новый номер — число принятых до него.
164
+ // Адрес приводится к строке один раз: она проверяется, она же
165
+ // встаёт в кадр — в копию описания, объект вызывающего не меняется.
166
+ // Фрейм — только по списку кадров.
167
+ index = 0;
168
+
169
+ for (var k = 0; k < items.length; k++) {
170
+ var src = String(items[k].src);
171
+ var url = parse(src);
172
+ // Схема — http(s), data:, blob: или та же, что у страницы: с диска
173
+ // свои файлы — file:, относительный адрес иначе не открылся бы.
174
+ var kind = url && (SCHEMES.test(url.protocol) || url.protocol === location.protocol) && (items[k].type === 'iframe' ? listed(url) : 'any');
175
+
176
+ if (!kind) {
177
+ G.warn('lightbox: frame address refused: ' + src + ' (iframe: GriffinJS.config.frames)');
178
+
179
+ if (k === at) return null;
180
+ continue;
181
+ }
182
+
183
+ if (k < at) index++;
184
+ list.push(Object.assign({}, items[k], { src: src, same: kind === 'host' }));
185
+ }
186
+
187
+ if (!list.length) return null;
188
+
100
189
  close();
101
190
 
102
191
  opts = opts || {};
103
- index = index || 0;
192
+ items = list;
104
193
 
105
194
  var n = items.length;
106
195
 
107
- if (n === 0) return null;
108
-
109
196
  dialog = element('dialog', 'gr-modal gr-lightbox', { 'aria-label': opts.label || 'Просмотр' });
110
197
 
111
198
  var trackEl = element('div', 'gr-track gr-lightbox-track', { tabindex: '0' });
@@ -169,6 +256,9 @@
169
256
 
170
257
  document.body.appendChild(dialog);
171
258
 
259
+ if (typeof dialog.showModal === 'function') dialog.showModal();
260
+ else dialog.setAttribute('open', '');
261
+
172
262
  if (n > 1) {
173
263
  track = G.track(trackEl, { loop: true, index: index });
174
264
  listen(trackEl, G.track.CHANGE, function (event) {
@@ -176,15 +266,10 @@
176
266
  });
177
267
  }
178
268
 
179
- if (typeof dialog.showModal === 'function') dialog.showModal();
180
- else dialog.setAttribute('open', '');
181
-
182
269
  return api;
183
270
  }
184
271
 
185
272
  function openFrom(node) {
186
- trigger = node;
187
-
188
273
  var group = groupOf(node);
189
274
  var items = [];
190
275
  var index = 0;
@@ -194,7 +279,11 @@
194
279
  items.push(itemOf(group[i]));
195
280
  }
196
281
 
197
- return open(items, index);
282
+ var opened = open(items, index);
283
+
284
+ if (opened) trigger = node;
285
+
286
+ return opened;
198
287
  }
199
288
 
200
289
  // --- Закрытие ----------------------------------------------------------------
@@ -241,10 +330,10 @@
241
330
 
242
331
  function onClick(event, node) {
243
332
  // Ctrl/Cmd/средняя кнопка — читатель хочет новую вкладку, а не окно.
244
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.button) return false;
333
+ // Кадр не открылся — ссылка работает как без скрипта.
334
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.button || !openFrom(node)) return false;
245
335
 
246
336
  if (event.preventDefault) event.preventDefault();
247
- openFrom(node);
248
337
 
249
338
  return true;
250
339
  }
@@ -27,7 +27,9 @@
27
27
  * закрывает и возвращает фокус на заголовок;
28
28
  * * закрытие щелчком мимо полосы и уходом фокуса;
29
29
  * * ajax-панели: data-gr-src на пункте грузится при первом открытии,
30
- * состояния loading → ready | error пишутся на пункт;
30
+ * состояния loading → ready | error пишутся на пункт. Только ответ
31
+ * своего origin с типом text/html: чужой адрес, data: и прочие
32
+ * схемы — error без запроса;
31
33
  * * закрытие с состоянием (Этап 48d): панель, чей уход тема анимирует,
32
34
  * не пропадает в первом же кадре — на пункт ставится
33
35
  * data-gr-state="closing", open снимается по transitionend/animationend
@@ -98,6 +100,44 @@
98
100
  return document.activeElement === node;
99
101
  }
100
102
 
103
+ // Фрагмент — только ответ своего origin с типом text/html: разметку
104
+ // из innerHTML браузер оживляет, и <img onerror> чужого ответа исполнился
105
+ // бы в origin страницы. Адрес — http(s) своего origin (у data: origin —
106
+ // строка 'null'); редирект на чужой отклоняет mode: 'same-origin'; тип
107
+ // ответа отсекает свой JSON или текст, отражающий запрос. Тип — целиком:
108
+ // text/htmlx — не HTML, а склейку двух заголовков («text/html,
109
+ // application/json») браузер читает по последнему типу.
110
+ function fragment(src) {
111
+ var url = null;
112
+
113
+ try { url = new URL(src, document.baseURI); } catch (e) { /* не адрес */ }
114
+
115
+ if (!url || !/^https?:$/.test(url.protocol) || url.origin !== location.origin) {
116
+ return Promise.reject(new Error('foreign origin'));
117
+ }
118
+
119
+ return fetch(url.href, { mode: 'same-origin' }).then(function (response) {
120
+ if (!response.ok) throw new Error('HTTP ' + response.status);
121
+ if (!/^text\/html\s*(;[^,]*)?$/i.test(response.headers.get('content-type'))) throw new Error('not text/html');
122
+
123
+ return response.text();
124
+ }).then(function (html) {
125
+ // Кусок разметки — без meta, base и link: refresh увёл бы страницу
126
+ // без щелчка, base сменил бы базу адресов всей страницы, а в куске
127
+ // разметки они — ошибка или подлог. В <template> они не срабатывают
128
+ // и снимаются до вставки; <script> куска не исполняется и так.
129
+ var tpl = document.createElement('template');
130
+
131
+ tpl.innerHTML = html;
132
+
133
+ var bad = tpl.content.querySelectorAll('meta, base, link');
134
+
135
+ for (var i = 0; i < bad.length; i++) bad[i].remove();
136
+
137
+ return tpl.content;
138
+ });
139
+ }
140
+
101
141
  // Длительность --gr-transition у узла, мс: «0.2s ease» → 200, «150ms» → 150,
102
142
  // пусто или 0s → 0. Одно чтение вычисленного стиля при закрытии.
103
143
  function duration(node) {
@@ -282,22 +322,18 @@
282
322
  item.loaded = true;
283
323
 
284
324
  if (typeof fetch !== 'function') {
285
- G.warn('мегаменю: fetch недоступен, панель ' + src + ' не загружена');
325
+ G.warn('megamenu: fetch is unavailable, panel ' + src + ' not loaded');
286
326
  setAttr(item.el, 'data-gr-state', 'error');
287
327
  return;
288
328
  }
289
329
 
290
330
  setAttr(item.el, 'data-gr-state', 'loading');
291
331
 
292
- fetch(src).then(function (response) {
293
- if (!response.ok) throw new Error('HTTP ' + response.status);
294
-
295
- return response.text();
296
- }).then(function (html) {
297
- item.panel.innerHTML = html;
332
+ fragment(src).then(function (html) {
333
+ item.panel.replaceChildren(html);
298
334
  setAttr(item.el, 'data-gr-state', 'ready');
299
335
  }, function (error) {
300
- G.warn('мегаменю: панель ' + src + ' не загружена: ' + (error && error.message));
336
+ G.warn('megamenu: panel ' + src + ' not loaded: ' + (error && error.message));
301
337
  setAttr(item.el, 'data-gr-state', 'error');
302
338
  });
303
339
  }
@@ -74,7 +74,7 @@
74
74
  var listeners = G.listeners();
75
75
  var fields = [];
76
76
 
77
- if (!inputs.length) throw new Error('внутри нет ни одного <input type="range">');
77
+ if (!inputs.length) throw new Error('no <input type="range"> inside');
78
78
 
79
79
  function doneOf(input) {
80
80
  var min = edge(input, 'min', 0);
@@ -168,11 +168,14 @@
168
168
 
169
169
  if (opts && opts.fields) {
170
170
  var selectors = String(opts.fields).split(',');
171
+ // Поля — <input> своей формы (без формы — документа): селекторы
172
+ // приходят и из данных.
173
+ var scope = G.closest(el, 'form') || document;
171
174
 
172
175
  for (var f = 0; f < inputs.length; f++) {
173
- var field = document.querySelector(selectors[f] || '#');
176
+ var field = scope.querySelector(selectors[f] || '#');
174
177
 
175
- if (!field) throw new Error('поле «' + selectors[f] + '» не найдено');
178
+ if (!field || field.tagName !== 'INPUT') throw new Error('field "' + selectors[f] + '" not found');
176
179
 
177
180
  fields.push(field);
178
181
  pull(f);
@@ -43,7 +43,7 @@
43
43
 
44
44
  var trackEl = el.querySelector('.gr-track') || el.firstElementChild;
45
45
 
46
- if (!trackEl) throw new Error('у слайдера нет дорожки');
46
+ if (!trackEl) throw new Error('the slider has no track');
47
47
 
48
48
  var track = G.track(trackEl, { loop: o.loop, rewind: o.rewind, step: o.step, engine: o.engine, index: o.index || 0 });
49
49
  var attrs = G.recorder();
@@ -89,7 +89,7 @@
89
89
  return el.querySelector('tbody');
90
90
  }
91
91
 
92
- if (!bodyOf() || !heads.length) throw new Error('нужны tbody и заголовки с aria-sort');
92
+ if (!bodyOf() || !heads.length) throw new Error('needs tbody and headers with aria-sort');
93
93
 
94
94
  var attrs = G.recorder();
95
95
  var events = G.listeners();
@@ -53,7 +53,7 @@
53
53
  var target = el.firstElementChild;
54
54
  var tip = el.querySelector('[role="tooltip"]') || el.lastElementChild;
55
55
 
56
- if (!target || !tip || tip === target) throw new Error('у подсказки нет якоря и текста');
56
+ if (!target || !tip || tip === target) throw new Error('the tooltip has no anchor and text');
57
57
 
58
58
  var shown = false;
59
59
  var timer = null;