griffincss-ui 0.24.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-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-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.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-number.js +3 -0
- package/dist/griffinjs-number.js.map +1 -0
- package/dist/griffinjs-range.js +1 -1
- package/dist/griffinjs-range.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 +126 -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/_datetime.scss +7 -0
- package/scss/griffinjs/fields/_number.scss +10 -0
- package/scss/griffinjs/fields/fields.scss +2 -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/datetime.js +103 -6
- package/src/griffinjs/fields/number.js +327 -0
- package/src/griffinjs/fields/validate.js +24 -3
- package/src/griffinjs/widgets/combobox.js +15 -2
- package/src/griffinjs/widgets/dialog.js +67 -2
- package/src/griffinjs/widgets/dropdown.js +94 -4
- package/src/griffinjs/widgets/megamenu.js +90 -5
- package/src/griffinjs/widgets/range.js +81 -7
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* GriffinJS — сумма с разрядами (Этап 43b).
|
|
3
|
+
*
|
|
4
|
+
* <input class="gr-input" type="text" inputmode="decimal" name="amount"
|
|
5
|
+
* min="0" max="1000000" data-gr-number="decimals: 2">
|
|
6
|
+
*
|
|
7
|
+
* На экране — текст с разделителями разрядов по локали документа
|
|
8
|
+
* («1 250 000,50» при lang="ru", «1,250,000.50» при lang="en"), число
|
|
9
|
+
* в точечной записи («1250000.50») — в скрытом спутнике с прежним именем:
|
|
10
|
+
* тот же приём, что у даты. type="number" разрядов не показывает,
|
|
11
|
+
* inputmode не форматирует, а маска знает только фиксированные слоты —
|
|
12
|
+
* поэтому это отдельное поле, а не режим маски.
|
|
13
|
+
*
|
|
14
|
+
* Разбор принимает любую запись: разделитель дроби — последний из «,»
|
|
15
|
+
* и «.», если это знак дроби локали или чужой знак не с тремя цифрами
|
|
16
|
+
* за ним («1.250.000» в ru — разряды, «1250.50» — дробь); знак разрядов
|
|
17
|
+
* локали дробью не бывает («1,250,000» и «1,25» в en), кроме записи,
|
|
18
|
+
* где разряды уже разделены пробелами («1 250 000,50»). Всё остальное,
|
|
19
|
+
* кроме цифр и минуса в начале, отбрасывается; минус принимается, только
|
|
20
|
+
* если min не задан или отрицателен. Лишние цифры дроби отбрасываются,
|
|
21
|
+
* не округляются: сумма — то, что набрано, а не то, что посчитано.
|
|
22
|
+
*
|
|
23
|
+
* Каретка — счётом принятых знаков слева от неё, как в маске: правки
|
|
24
|
+
* перехватываются в beforeinput, значение пишется целиком, Backspace
|
|
25
|
+
* и Delete перешагивают разделители разрядов. На композиции, автозаполнении
|
|
26
|
+
* и отмене виджет отступает и приводит значение к формату по input —
|
|
27
|
+
* единственный случай, когда каретка уходит в конец.
|
|
28
|
+
*
|
|
29
|
+
* Границы min/max с поля переезжают в спутник: там, где платформа умеет,
|
|
30
|
+
* сообщение о выходе за них берётся у неё, иначе — из параметра range.
|
|
31
|
+
* Красным (aria-invalid) — только по уходу фокуса, светлеет, как только
|
|
32
|
+
* значение вернулось в границы. Пустое поле — required платформы.
|
|
33
|
+
* Дробь дополняется нулями до decimals по уходу фокуса и при записи
|
|
34
|
+
* из API; спутник несёт число как есть. destroy() возвращает имя
|
|
35
|
+
* и число в поле и снимает всё своё.
|
|
36
|
+
*
|
|
37
|
+
* Валюта и единица — не здесь: .gr-input-addon рядом. База без скрипта —
|
|
38
|
+
* обычное текстовое поле: сервер получит то, что набрано, разбор на
|
|
39
|
+
* сервере обязателен в любом случае.
|
|
40
|
+
*
|
|
41
|
+
* Виджет пишет value и валидность контрола — расширение инварианта 7,
|
|
42
|
+
* объявленное в docs/griffinjs-architecture.html («Инварианты»).
|
|
43
|
+
*/
|
|
44
|
+
(function (G) {
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
var DEFAULTS = { decimals: 0, range: 'Значение вне допустимых границ' };
|
|
48
|
+
|
|
49
|
+
function lang() {
|
|
50
|
+
return (document.documentElement && document.documentElement.getAttribute('lang'))
|
|
51
|
+
|| (typeof navigator !== 'undefined' && navigator.language) || 'en';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var known = {};
|
|
55
|
+
|
|
56
|
+
// Знаки локали: разряды и дробь — из Intl; цифры всегда латинские,
|
|
57
|
+
// иначе разбор не узнал бы собственный вывод. Считается раз на локаль.
|
|
58
|
+
function marks(locale) {
|
|
59
|
+
if (known[locale]) return known[locale];
|
|
60
|
+
|
|
61
|
+
var group = '';
|
|
62
|
+
var decimal = '.';
|
|
63
|
+
var nf;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
nf = new Intl.NumberFormat(locale, { numberingSystem: 'latn' });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
nf = new Intl.NumberFormat('en');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var parts = nf.formatToParts(12345.6);
|
|
72
|
+
|
|
73
|
+
for (var i = 0; i < parts.length; i++) {
|
|
74
|
+
if (parts[i].type === 'group') group = parts[i].value;
|
|
75
|
+
if (parts[i].type === 'decimal') decimal = parts[i].value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (known[locale] = { group: group, decimal: decimal, nf: nf });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Текст → {neg, int, frac, point, value}: value — число в точечной
|
|
82
|
+
// записи или пустая строка; point — знак дроби набран (дробь может быть
|
|
83
|
+
// ещё пустой). Ведущие нули снимаются.
|
|
84
|
+
function parse(text, locale, decimals, signed) {
|
|
85
|
+
var m = marks(locale);
|
|
86
|
+
var s = String(text || '');
|
|
87
|
+
var neg = signed && /^\s*[-\u2212]/.test(s);
|
|
88
|
+
var at = Math.max(s.lastIndexOf(','), s.lastIndexOf('.'));
|
|
89
|
+
var point = false;
|
|
90
|
+
|
|
91
|
+
if (at !== -1) {
|
|
92
|
+
var ch = s.charAt(at);
|
|
93
|
+
var after = s.slice(at + 1).replace(/\D/g, '').length;
|
|
94
|
+
|
|
95
|
+
// Знак разрядов локали — дробь, только если разряды в тексте уже
|
|
96
|
+
// разделены пробелами: «1 250 000,50» из русского буфера в en.
|
|
97
|
+
point = ch === m.decimal || (after !== 3 && (ch !== m.group || /\d[\s\u00a0\u202f]\d/.test(s)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var head = point ? s.slice(0, at) : s;
|
|
101
|
+
var int = head.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
|
|
102
|
+
var frac = point ? s.slice(at + 1).replace(/\D/g, '').slice(0, decimals) : '';
|
|
103
|
+
|
|
104
|
+
if (!decimals) point = false;
|
|
105
|
+
if (!int && frac) int = '0';
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
neg: neg, int: int, frac: frac, point: point,
|
|
109
|
+
value: int ? (neg ? '-' : '') + int + (frac ? '.' + frac : '') : ''
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Разбор → текст на экране: разряды — у Intl, дробь — как набрана
|
|
114
|
+
// (pad — дополнена нулями до decimals).
|
|
115
|
+
function format(p, locale, decimals, pad) {
|
|
116
|
+
var m = marks(locale);
|
|
117
|
+
var out = p.neg ? '-' : '';
|
|
118
|
+
|
|
119
|
+
if (p.int) out += m.nf.format(Number(p.int));
|
|
120
|
+
|
|
121
|
+
var frac = p.frac;
|
|
122
|
+
|
|
123
|
+
if (pad && p.int && decimals) while (frac.length < decimals) frac += '0';
|
|
124
|
+
if (frac || (p.point && !pad)) out += m.decimal + frac;
|
|
125
|
+
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fire(el, type) {
|
|
130
|
+
try {
|
|
131
|
+
el.dispatchEvent(new Event(type, { bubbles: true }));
|
|
132
|
+
} catch (e) { /* окружение без Event */ }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
G.defineWidget('number', function (el, opts) {
|
|
136
|
+
var o = G.merge(DEFAULTS, opts);
|
|
137
|
+
var attrs = G.recorder();
|
|
138
|
+
var events = G.listeners();
|
|
139
|
+
var locale = lang();
|
|
140
|
+
var decimals = o.decimals > 0 ? o.decimals : 0;
|
|
141
|
+
var signed = !(parseFloat(el.getAttribute('min')) >= 0);
|
|
142
|
+
var significant = new RegExp('[\\d\\-\\u2212' + marks(locale).decimal.replace(/[.\\]/g, '\\$&') + ']');
|
|
143
|
+
var companion = null;
|
|
144
|
+
var last = null; // последнее записанное значение на экране
|
|
145
|
+
var told = null; // последнее число, о котором сказано событием
|
|
146
|
+
var focused = null; // значение на момент фокуса — для change
|
|
147
|
+
var bad = false; // вне границ
|
|
148
|
+
|
|
149
|
+
// Позиция каретки за k-м принятым знаком.
|
|
150
|
+
function caret(text, k) {
|
|
151
|
+
for (var i = 0; i < text.length && k > 0; i++) if (significant.test(text.charAt(i))) k--;
|
|
152
|
+
|
|
153
|
+
return i;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function size(text) {
|
|
157
|
+
var p = parse(text, locale, decimals, signed);
|
|
158
|
+
|
|
159
|
+
return (p.neg ? 1 : 0) + p.int.length + (p.point ? 1 : 0) + p.frac.length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function check() {
|
|
163
|
+
var value = companion.value;
|
|
164
|
+
var message = '';
|
|
165
|
+
|
|
166
|
+
if (value) {
|
|
167
|
+
if (companion.validity) {
|
|
168
|
+
if (!companion.validity.valid) message = companion.validationMessage || o.range;
|
|
169
|
+
} else {
|
|
170
|
+
var min = parseFloat(el.getAttribute('min'));
|
|
171
|
+
var max = parseFloat(el.getAttribute('max'));
|
|
172
|
+
var n = parseFloat(value);
|
|
173
|
+
|
|
174
|
+
if (n < min || n > max) message = o.range;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
bad = !!message;
|
|
179
|
+
if (typeof el.setCustomValidity === 'function') el.setCustomValidity(message);
|
|
180
|
+
if (!bad) attrs.set(el, 'aria-invalid', null);
|
|
181
|
+
|
|
182
|
+
// О первом значении — с разметки — не сообщается: его никто не менял.
|
|
183
|
+
if (value !== told) {
|
|
184
|
+
var first = told === null;
|
|
185
|
+
|
|
186
|
+
told = value;
|
|
187
|
+
if (!first) G.emit('griffin:change', el, { value: value });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Запись: текст на экране, число в спутнике, каретка за k-м знаком
|
|
192
|
+
// (без k каретка не трогается — при записи вне фокуса ей всё равно).
|
|
193
|
+
function write(text, k, pad) {
|
|
194
|
+
var p = parse(text, locale, decimals, signed);
|
|
195
|
+
var next = format(p, locale, decimals, pad);
|
|
196
|
+
|
|
197
|
+
if (el.value !== next) el.value = next;
|
|
198
|
+
last = next;
|
|
199
|
+
companion.value = p.value;
|
|
200
|
+
check();
|
|
201
|
+
|
|
202
|
+
if (k !== undefined && typeof el.setSelectionRange === 'function') {
|
|
203
|
+
var at = caret(next, k);
|
|
204
|
+
|
|
205
|
+
el.setSelectionRange(at, at);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Позиция знака, который снимает Backspace (последний перед кареткой)
|
|
210
|
+
// и Delete (первый от каретки); разделители между ними перешагиваются.
|
|
211
|
+
function before(at) {
|
|
212
|
+
while (at > 0 && !significant.test(el.value.charAt(at - 1))) at--;
|
|
213
|
+
|
|
214
|
+
return at > 0 ? at - 1 : 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function after(at) {
|
|
218
|
+
var text = el.value;
|
|
219
|
+
|
|
220
|
+
while (at < text.length && !significant.test(text.charAt(at))) at++;
|
|
221
|
+
|
|
222
|
+
return at < text.length ? at + 1 : at;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function onBeforeInput(event) {
|
|
226
|
+
var type = event.inputType || '';
|
|
227
|
+
|
|
228
|
+
if (event.isComposing || type.indexOf('Composition') !== -1) return;
|
|
229
|
+
|
|
230
|
+
var start = el.selectionStart;
|
|
231
|
+
var end = el.selectionEnd;
|
|
232
|
+
|
|
233
|
+
if (typeof start !== 'number' || typeof end !== 'number') return;
|
|
234
|
+
|
|
235
|
+
var insert = '';
|
|
236
|
+
|
|
237
|
+
if (type === 'insertText' || type === 'insertFromPaste' || type === 'insertFromDrop') {
|
|
238
|
+
insert = event.data || (event.dataTransfer ? event.dataTransfer.getData('text') : '') || '';
|
|
239
|
+
} else if (type === 'deleteContentBackward') {
|
|
240
|
+
if (start === end) start = before(start);
|
|
241
|
+
} else if (type === 'deleteContentForward' || type === 'deleteByCut') {
|
|
242
|
+
if (start === end) end = after(end);
|
|
243
|
+
} else {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
|
|
249
|
+
var text = el.value;
|
|
250
|
+
var head = text.slice(0, start) + insert;
|
|
251
|
+
var was = el.value;
|
|
252
|
+
|
|
253
|
+
write(head + text.slice(end), size(head));
|
|
254
|
+
|
|
255
|
+
if (el.value !== was) fire(el, 'input');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function onInput(event) {
|
|
259
|
+
if (event.isComposing) return;
|
|
260
|
+
if (el.value !== last) write(el.value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function onCompositionEnd() {
|
|
264
|
+
write(el.value);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function onFocus() {
|
|
268
|
+
focused = el.value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function onBlur() {
|
|
272
|
+
write(el.value, undefined, true);
|
|
273
|
+
|
|
274
|
+
if (bad) attrs.set(el, 'aria-invalid', 'true');
|
|
275
|
+
if (focused !== null && el.value !== focused) fire(el, 'change');
|
|
276
|
+
focused = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Спутник: число под прежним именем и границы — платформа проверяет их сама.
|
|
280
|
+
companion = document.createElement('input');
|
|
281
|
+
companion.setAttribute('type', 'number');
|
|
282
|
+
companion.setAttribute('hidden', '');
|
|
283
|
+
companion.setAttribute('step', el.getAttribute('step') || 'any');
|
|
284
|
+
|
|
285
|
+
for (var i = 0, copy = ['name', 'min', 'max']; i < copy.length; i++) {
|
|
286
|
+
var v = el.getAttribute(copy[i]);
|
|
287
|
+
|
|
288
|
+
if (v !== null) companion.setAttribute(copy[i], v);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
el.parentNode.insertBefore(companion, el.nextSibling);
|
|
292
|
+
attrs.set(el, 'name', null);
|
|
293
|
+
if (!el.getAttribute('inputmode')) attrs.set(el, 'inputmode', 'decimal');
|
|
294
|
+
|
|
295
|
+
events.add(el, 'beforeinput', onBeforeInput);
|
|
296
|
+
events.add(el, 'input', onInput);
|
|
297
|
+
events.add(el, 'compositionend', onCompositionEnd);
|
|
298
|
+
events.add(el, 'focus', onFocus);
|
|
299
|
+
events.add(el, 'blur', onBlur);
|
|
300
|
+
|
|
301
|
+
write(el.value, undefined, true);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
el: el,
|
|
305
|
+
value: function () { return companion.value; },
|
|
306
|
+
set: function (value) {
|
|
307
|
+
var was = el.value;
|
|
308
|
+
|
|
309
|
+
write(String(value), undefined, true);
|
|
310
|
+
if (el.value !== was) fire(el, 'input');
|
|
311
|
+
},
|
|
312
|
+
destroy: function () {
|
|
313
|
+
events.removeAll();
|
|
314
|
+
|
|
315
|
+
var value = companion.value;
|
|
316
|
+
|
|
317
|
+
companion.remove();
|
|
318
|
+
attrs.restore();
|
|
319
|
+
el.value = value;
|
|
320
|
+
|
|
321
|
+
if (typeof el.setCustomValidity === 'function') el.setCustomValidity('');
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
G.number = { parse: parse, format: format, marks: marks };
|
|
327
|
+
})(typeof window !== 'undefined' && window.GriffinJS ? window.GriffinJS : require('../core/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
|
|
|
@@ -39,6 +39,13 @@
|
|
|
39
39
|
* удаление тега или Backspace в пустом поле снимает выбор. Значение всегда
|
|
40
40
|
* в <select> — форма отправляет его как без скрипта; о смене он сообщает
|
|
41
41
|
* событием change.
|
|
42
|
+
*
|
|
43
|
+
* Свободные теги (Этап 42f) — флаг free рядом с multiple: Enter на непустом
|
|
44
|
+
* поле без активной позиции создаёт <option selected> со значением, равным
|
|
45
|
+
* тексту (ключевые слова, адреса писем). Дубликата по значению нет —
|
|
46
|
+
* существующая позиция просто выбирается. На сервер значение уходит
|
|
47
|
+
* текстом, проверять его — там. Без multiple флаг смысла не имеет: текст
|
|
48
|
+
* поля и есть значение, — и пропускается с предупреждением.
|
|
42
49
|
*/
|
|
43
50
|
(function (G) {
|
|
44
51
|
'use strict';
|
|
@@ -54,6 +61,7 @@
|
|
|
54
61
|
highlight: true,
|
|
55
62
|
cache: true,
|
|
56
63
|
multiple: false, // выбранное — теги, значение — в <select multiple>
|
|
64
|
+
free: false, // Enter без позиции — новый тег из текста (только с multiple)
|
|
57
65
|
remove: 'Убрать {label}'
|
|
58
66
|
};
|
|
59
67
|
|
|
@@ -137,6 +145,7 @@
|
|
|
137
145
|
var choice = o.multiple ? el.querySelector('select[multiple]') : null;
|
|
138
146
|
|
|
139
147
|
if (o.multiple && !choice) throw new Error('для multiple нужен <select multiple> внутри');
|
|
148
|
+
if (o.free && !choice) { G.warn('комбобокс: free без multiple смысла не имеет — флаг пропущен'); o.free = false; }
|
|
140
149
|
|
|
141
150
|
var list = el.querySelector('[role="listbox"]');
|
|
142
151
|
var created = false;
|
|
@@ -452,8 +461,12 @@
|
|
|
452
461
|
activate(key === 'Home' ? 0 : items.length - 1, true);
|
|
453
462
|
event.preventDefault();
|
|
454
463
|
} else if (key === 'Enter') {
|
|
455
|
-
|
|
456
|
-
|
|
464
|
+
var text = input.value.trim();
|
|
465
|
+
|
|
466
|
+
if (opened && active >= 0) select(active);
|
|
467
|
+
else if (o.free && text) { add(text, text); input.value = ''; close(); }
|
|
468
|
+
else return;
|
|
469
|
+
|
|
457
470
|
event.preventDefault();
|
|
458
471
|
} else if (key === 'Escape') {
|
|
459
472
|
if (!opened) return;
|
|
@@ -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 — окно показывается сразу.
|