griffincss-ui 0.23.1 → 0.25.0-dev
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/griffinjs-combobox.js +1 -1
- package/dist/griffinjs-combobox.js.map +1 -1
- package/dist/griffinjs-core.js +1 -1
- package/dist/griffinjs-datetime.js +1 -1
- package/dist/griffinjs-datetime.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-mask.js +1 -1
- package/dist/griffinjs-mask.js.map +1 -1
- package/dist/griffinjs-number.js +3 -0
- package/dist/griffinjs-number.js.map +1 -0
- 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.js +1 -1
- package/dist/griffinjs.js.map +1 -1
- package/package.json +3 -3
- package/scss/_button.scss +21 -0
- package/scss/_choice.scss +90 -0
- package/scss/_steps.scss +6 -2
- package/scss/griffincss-ui-scoped.scss +4 -2
- package/scss/griffincss-ui.scss +9 -2
- package/scss/griffinjs/fields/_datetime.scss +7 -0
- package/scss/griffinjs/fields/_number.scss +12 -0
- package/scss/griffinjs/fields/fields.scss +1 -0
- package/src/griffincss-ui.js +2 -2
- package/src/griffinjs/core/griffinjs-core.js +2 -2
- package/src/griffinjs/fields/datetime.js +103 -6
- package/src/griffinjs/fields/mask.js +28 -7
- package/src/griffinjs/fields/number.js +327 -0
- package/src/griffinjs/fields/phone.js +17 -3
- package/src/griffinjs/widgets/combobox.js +15 -2
- package/src/griffinjs/widgets/range.js +81 -7
|
@@ -166,6 +166,13 @@
|
|
|
166
166
|
outline-offset: -2px;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Пара «с — по»: дни между концами — той же заливкой, что у наведения,
|
|
170
|
+
// концы — как выбранный день; между ними и по бокам сетки остаётся
|
|
171
|
+
// тот же зазор, что и у всех клеток.
|
|
172
|
+
.gr-datetime-day[data-gr-in-range] {
|
|
173
|
+
background-color: hsl(var(--gr-hsl-accent), 12%);
|
|
174
|
+
}
|
|
175
|
+
|
|
169
176
|
.gr-datetime-day[aria-selected="true"] {
|
|
170
177
|
background-color: var(--gr-color-accent);
|
|
171
178
|
color: var(--gr-color-on-accent);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// GriffinJS — сумма с разрядами (Этап 43b)
|
|
2
|
+
// Своих стилей у поля нет: на экране обычный .gr-input с текстом. Единственное
|
|
3
|
+
// правило — про спутник. Скрытый input с числом встаёт сразу за полем
|
|
4
|
+
// (у суммы — всегда, у даты — на указателе мыши), и в .gr-input-group
|
|
5
|
+
// с аддоном после поля он был бы последним ребёнком: поле теряло бы
|
|
6
|
+
// скругление края. Скругление возвращается тому, за кем стоит спутник.
|
|
7
|
+
// Без :has() остаётся прямой угол у поля — деградация видимая, но мягкая.
|
|
8
|
+
|
|
9
|
+
.gr-input-group > :has(+ [hidden]:last-child) {
|
|
10
|
+
border-start-end-radius: var(--gr-radius);
|
|
11
|
+
border-end-end-radius: var(--gr-radius);
|
|
12
|
+
}
|
package/src/griffincss-ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Griffincss UI — Runtime v0.
|
|
2
|
+
* Griffincss UI — Runtime v0.25.0-dev
|
|
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.25.0-dev';
|
|
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.25.0-dev
|
|
3
3
|
* Опциональный слой виджетов с состоянием поверх Griffincss: слайдер,
|
|
4
4
|
* галерея, лайтбокс, параллакс, мегаменю. Подключается одной строкой
|
|
5
5
|
* и одним файлом; страница без него — законное и рабочее состояние.
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
})(function () {
|
|
39
39
|
'use strict';
|
|
40
40
|
|
|
41
|
-
var VERSION = '0.
|
|
41
|
+
var VERSION = '0.25.0-dev';
|
|
42
42
|
|
|
43
43
|
// Реестры. Модуль зовёт defineEngine/defineWidget; ядро — единственное
|
|
44
44
|
// место, которое знает, как их применить.
|
|
@@ -46,6 +46,22 @@
|
|
|
46
46
|
* invalid, range — сообщения проверки; label, prev, next, year, time —
|
|
47
47
|
* подписи панели.
|
|
48
48
|
*
|
|
49
|
+
* Диапазон «с — по» (Этап 43a) — пара нативных полей, связанная атрибутом:
|
|
50
|
+
* <input type="date" name="from" data-gr-datetime="to: #until">
|
|
51
|
+
* <input type="date" name="until" id="until" data-gr-datetime>
|
|
52
|
+
* Одного контрола с двумя концами нет намеренно: база без скрипта — два
|
|
53
|
+
* нативных поля, и панель одного поля пишет одно поле. Связка строится
|
|
54
|
+
* на готовом: значение «с» становится min у «по», значение «по» — max
|
|
55
|
+
* у «с», через собственный API соседа (limit), — и проверка границ,
|
|
56
|
+
* сообщения и aria-disabled в панели работают без новой логики; на тач-
|
|
57
|
+
* устройстве системный календарь сам скроет недоступное. Авторская
|
|
58
|
+
* граница остаётся, если она уже; очистка и destroy() возвращают её.
|
|
59
|
+
* Порядок дат виджет не толкает: «по» раньше «с» — ошибка ввода, и её
|
|
60
|
+
* показывает проверка границ, а не молчаливая правка второго поля.
|
|
61
|
+
* Сверх границ — только панель: пустое поле открывается на месяце
|
|
62
|
+
* соседа, оба конца — aria-selected, дни между ними — data-gr-in-range
|
|
63
|
+
* (красит CSS панели), и range: {from, to} в detail griffin:change обоих.
|
|
64
|
+
*
|
|
49
65
|
* Виджет пишет value контрола — расширение инварианта 7, объявленное
|
|
50
66
|
* в docs/griffinjs-architecture.html («Инварианты»).
|
|
51
67
|
*/
|
|
@@ -209,6 +225,11 @@
|
|
|
209
225
|
return node;
|
|
210
226
|
}
|
|
211
227
|
|
|
228
|
+
function attr(node, name, value) {
|
|
229
|
+
if (value) node.setAttribute(name, value);
|
|
230
|
+
else node.removeAttribute(name);
|
|
231
|
+
}
|
|
232
|
+
|
|
212
233
|
G.defineWidget('datetime', { needs: ['mask', 'anchor'] }, function (el, opts) {
|
|
213
234
|
var o = G.merge(DEFAULTS, opts);
|
|
214
235
|
var native = el.getAttribute('type');
|
|
@@ -241,11 +262,52 @@
|
|
|
241
262
|
var view = null; // первое число показанного месяца
|
|
242
263
|
var active = null; // активный день панели, 'YYYY-MM-DD'
|
|
243
264
|
var last = null; // последнее ISO, о котором сказано событием
|
|
265
|
+
var peer = null; // второе поле пары «с — по»
|
|
266
|
+
var role = ''; // 'from' | 'to' — кто это поле в паре
|
|
267
|
+
var own = { min: el.getAttribute('min'), max: el.getAttribute('max') }; // авторские границы
|
|
244
268
|
|
|
245
269
|
function iso() {
|
|
246
270
|
return companion ? companion.value : el.value;
|
|
247
271
|
}
|
|
248
272
|
|
|
273
|
+
// --- Пара «с — по» ------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
function range() {
|
|
276
|
+
var mine = iso();
|
|
277
|
+
var theirs = peer.value();
|
|
278
|
+
|
|
279
|
+
return role === 'from' ? { from: mine, to: theirs } : { from: theirs, to: mine };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Граница на поле и спутнике; проверка и панель узнают о ней тем же
|
|
283
|
+
// путём, что о всякой другой.
|
|
284
|
+
function apply(name, value) {
|
|
285
|
+
attr(el, name, value);
|
|
286
|
+
|
|
287
|
+
if (companion) { attr(companion, name, value); check(); }
|
|
288
|
+
if (opened && body) render(view);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Граница от соседа: авторская остаётся, если она уже; пустое
|
|
292
|
+
// значение возвращает авторскую.
|
|
293
|
+
function limit(name, value) {
|
|
294
|
+
var mine = own[name];
|
|
295
|
+
|
|
296
|
+
apply(name, !value ? mine : mine && (name === 'min' ? mine > value : mine < value) ? mine : value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function push() {
|
|
300
|
+
if (peer) peer.limit(role === 'from' ? 'min' : 'max', iso());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function unpair() {
|
|
304
|
+
var name = role === 'from' ? 'max' : 'min';
|
|
305
|
+
|
|
306
|
+
peer = null;
|
|
307
|
+
role = '';
|
|
308
|
+
apply(name, own[name]);
|
|
309
|
+
}
|
|
310
|
+
|
|
249
311
|
// --- Проверка ---------------------------------------------------------------
|
|
250
312
|
|
|
251
313
|
function check() {
|
|
@@ -275,7 +337,8 @@
|
|
|
275
337
|
|
|
276
338
|
if (value !== last) {
|
|
277
339
|
last = value;
|
|
278
|
-
G.emit('griffin:change', el, { value: value });
|
|
340
|
+
G.emit('griffin:change', el, peer ? { value: value, range: range() } : { value: value });
|
|
341
|
+
push();
|
|
279
342
|
}
|
|
280
343
|
}
|
|
281
344
|
|
|
@@ -365,6 +428,11 @@
|
|
|
365
428
|
if (companion) open();
|
|
366
429
|
}
|
|
367
430
|
|
|
431
|
+
// Нативное поле: значение пишет платформа, и сосед по паре узнаёт о нём здесь.
|
|
432
|
+
function onNativeInput() {
|
|
433
|
+
if (!companion) push();
|
|
434
|
+
}
|
|
435
|
+
|
|
368
436
|
function onInput() {
|
|
369
437
|
check();
|
|
370
438
|
|
|
@@ -522,7 +590,10 @@
|
|
|
522
590
|
var m = base.getMonth();
|
|
523
591
|
var offset = (new Date(y, m, 1).getDay() - fd + 7) % 7;
|
|
524
592
|
var start = new Date(y, m, 1 - offset);
|
|
525
|
-
|
|
593
|
+
// Концы отрезка: у одиночного поля оба — его значение.
|
|
594
|
+
var ends = peer ? range() : { from: iso(), to: iso() };
|
|
595
|
+
var lo = dateOf(ends.from);
|
|
596
|
+
var hi = dateOf(ends.to);
|
|
526
597
|
var today = isoDate(new Date());
|
|
527
598
|
var min = dateOf(el.getAttribute('min'));
|
|
528
599
|
var max = dateOf(el.getAttribute('max'));
|
|
@@ -543,8 +614,9 @@
|
|
|
543
614
|
var cell = element('div', 'gr-datetime-day', { role: 'gridcell', id: panel.id + '-' + key, 'data-gr-date': key });
|
|
544
615
|
|
|
545
616
|
cell.textContent = String(date.getDate());
|
|
546
|
-
cell.setAttribute('aria-selected', key ===
|
|
617
|
+
cell.setAttribute('aria-selected', key === lo || key === hi ? 'true' : 'false');
|
|
547
618
|
|
|
619
|
+
if (lo && hi && lo < key && key < hi) cell.setAttribute('data-gr-in-range', '');
|
|
548
620
|
if (key === today) cell.setAttribute('aria-current', 'date');
|
|
549
621
|
if ((min && key < min) || (max && key > max)) cell.setAttribute('aria-disabled', 'true');
|
|
550
622
|
if (key === active) { cell.setAttribute('data-gr-state', 'active'); descendant = cell.id; }
|
|
@@ -566,7 +638,8 @@
|
|
|
566
638
|
opened = true;
|
|
567
639
|
active = null;
|
|
568
640
|
|
|
569
|
-
|
|
641
|
+
// Пустое поле пары открывается на месяце соседа.
|
|
642
|
+
var d = dateOf(iso()) || (peer ? dateOf(peer.value()) : '');
|
|
570
643
|
|
|
571
644
|
if (body) render(d ? fromIso(d) : new Date());
|
|
572
645
|
if (clock) clock.value = timeOf(iso());
|
|
@@ -682,21 +755,33 @@
|
|
|
682
755
|
|
|
683
756
|
events.add(el, 'pointerdown', onPointerDown);
|
|
684
757
|
events.add(el, 'click', onClick);
|
|
758
|
+
events.add(el, 'input', onNativeInput);
|
|
685
759
|
|
|
686
|
-
|
|
760
|
+
var api = {
|
|
687
761
|
el: el,
|
|
688
762
|
open: open,
|
|
689
763
|
close: close,
|
|
690
764
|
value: iso,
|
|
691
765
|
set: function (value) {
|
|
692
766
|
if (companion) write(value);
|
|
693
|
-
else el.value = value;
|
|
767
|
+
else { el.value = value; push(); }
|
|
694
768
|
},
|
|
769
|
+
// Пара: limit — граница от соседа (единственный путь, которым виджет
|
|
770
|
+
// пишет min/max чужого поля), pair — «other — моё „с“», unpair — развод.
|
|
771
|
+
limit: limit,
|
|
772
|
+
pair: function (other) {
|
|
773
|
+
peer = other;
|
|
774
|
+
role = 'to';
|
|
775
|
+
push();
|
|
776
|
+
},
|
|
777
|
+
unpair: unpair,
|
|
695
778
|
get panel() { return panel; },
|
|
696
779
|
destroy: function () {
|
|
697
780
|
restore();
|
|
698
781
|
events.removeAll();
|
|
699
782
|
|
|
783
|
+
if (peer) { var other = peer; unpair(); other.unpair(); }
|
|
784
|
+
|
|
700
785
|
if (panel) {
|
|
701
786
|
if (G.anchor) G.anchor.clear(panel);
|
|
702
787
|
panel.remove();
|
|
@@ -704,6 +789,18 @@
|
|
|
704
789
|
}
|
|
705
790
|
}
|
|
706
791
|
};
|
|
792
|
+
|
|
793
|
+
// Сосед по паре: уже поднятый — как есть, иначе поднимается здесь;
|
|
794
|
+
// сканер на нём потом найдёт живой экземпляр.
|
|
795
|
+
if (o.to && mode !== 'time') {
|
|
796
|
+
var target = document.querySelector(o.to);
|
|
797
|
+
var other = target && G.mount(target, 'datetime');
|
|
798
|
+
|
|
799
|
+
if (other) { peer = other; role = 'from'; other.pair(api); push(); }
|
|
800
|
+
else G.warn('datetime: поле «по» не найдено — ' + o.to);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return api;
|
|
707
804
|
});
|
|
708
805
|
|
|
709
806
|
G.datetime = { layout: layout, parse: parse, format: format };
|
|
@@ -19,9 +19,19 @@
|
|
|
19
19
|
* fill — знак свободного слота в ней, «_» по умолчанию.
|
|
20
20
|
*
|
|
21
21
|
* База без скрипта: pattern и inputmode на элементе — платформа проверяет
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* сама. Авторский pattern маска не трогает и не заменяет; из маски она берёт
|
|
23
|
+
* два удобства — подсказку и inputmode="numeric" для маски из одних цифр,
|
|
24
|
+
* и то, если автор не задал своих.
|
|
25
|
+
*
|
|
26
|
+
* Недобор. Непустое значение с незаполненными обязательными слотами — своя
|
|
27
|
+
* ошибка проверки: setCustomValidity(incomplete) ставится сразу, поэтому
|
|
28
|
+
* checkValidity() даёт false и форма не уходит с половиной номера. На полном
|
|
29
|
+
* значении custom-error пуст, и единственным судьёй снова становится pattern.
|
|
30
|
+
* Пустое поле маска не трогает: пустое — это required платформы, и поле
|
|
31
|
+
* без required обязательным не становится. Красным (aria-invalid) поле
|
|
32
|
+
* помечается ТОЛЬКО по уходу фокуса и светлеет, как только значение стало
|
|
33
|
+
* полным: ругаться на человека, пока он печатает, — не проверка. Там, где
|
|
34
|
+
* недобор законен, выход не требует нового API — токен ? в формате.
|
|
25
35
|
*
|
|
26
36
|
* Каретка. Правки перехватываются в beforeinput: новое значение считается
|
|
27
37
|
* здесь, пишется целиком, и каретка встаёт за только что введённый знак —
|
|
@@ -31,16 +41,17 @@
|
|
|
31
41
|
* браузер делает своё, а маска потом приводит значение к формату по input
|
|
32
42
|
* или compositionend. Это единственный случай, когда каретка уходит в конец.
|
|
33
43
|
*
|
|
34
|
-
* Маска пишет value контрола — первый такой виджет в слое.
|
|
35
|
-
* инварианта 7 объявлено в docs/griffinjs-architecture.html
|
|
36
|
-
* и в правилах правки
|
|
44
|
+
* Маска пишет value контрола и его валидность — первый такой виджет в слое.
|
|
45
|
+
* Расширение инварианта 7 объявлено в docs/griffinjs-architecture.html
|
|
46
|
+
* («Инварианты») и в правилах правки слоя: пишешь value — пиши и валидность,
|
|
47
|
+
* и возвращай её в destroy(). После каждой своей записи она шлёт input
|
|
37
48
|
* (а по уходу фокуса — change), поэтому фреймворк узнаёт о значении
|
|
38
49
|
* так же, как о набранном руками.
|
|
39
50
|
*/
|
|
40
51
|
(function (G) {
|
|
41
52
|
'use strict';
|
|
42
53
|
|
|
43
|
-
var DEFAULTS = { format: '', hint: true, fill: '_' };
|
|
54
|
+
var DEFAULTS = { format: '', hint: true, fill: '_', incomplete: 'Значение набрано не полностью' };
|
|
44
55
|
|
|
45
56
|
// Что принимает слот. Буква — любая по Юникоду: маска номерного знака
|
|
46
57
|
// на кириллице не хуже латинской.
|
|
@@ -157,6 +168,7 @@
|
|
|
157
168
|
var tokens;
|
|
158
169
|
var last = null; // последнее записанное значение
|
|
159
170
|
var focused = null; // значение на момент фокуса — для change
|
|
171
|
+
var short = false; // набрано, но не до конца
|
|
160
172
|
var ownHint = o.hint && !el.getAttribute('placeholder');
|
|
161
173
|
var ownMode = !el.getAttribute('inputmode');
|
|
162
174
|
|
|
@@ -170,6 +182,12 @@
|
|
|
170
182
|
|
|
171
183
|
attrs.set(el, 'data-gr-state', !result.raw ? 'empty' : result.complete ? 'complete' : 'partial');
|
|
172
184
|
|
|
185
|
+
// Недобор — своя ошибка; пустое поле остаётся заботой required.
|
|
186
|
+
short = !!result.raw && !result.complete;
|
|
187
|
+
|
|
188
|
+
if (typeof el.setCustomValidity === 'function') el.setCustomValidity(short ? o.incomplete : '');
|
|
189
|
+
if (!short) attrs.set(el, 'aria-invalid', null);
|
|
190
|
+
|
|
173
191
|
if (k !== undefined && typeof el.setSelectionRange === 'function') {
|
|
174
192
|
var at = caret(result, k);
|
|
175
193
|
|
|
@@ -264,6 +282,7 @@
|
|
|
264
282
|
// правку он не видел. Событие шлётся здесь по тому же правилу —
|
|
265
283
|
// значение по уходу фокуса отличается от значения на входе.
|
|
266
284
|
function onBlur() {
|
|
285
|
+
if (short) attrs.set(el, 'aria-invalid', 'true');
|
|
267
286
|
if (focused !== null && el.value !== focused) fire(el, 'change');
|
|
268
287
|
focused = null;
|
|
269
288
|
}
|
|
@@ -298,6 +317,8 @@
|
|
|
298
317
|
destroy: function () {
|
|
299
318
|
events.removeAll();
|
|
300
319
|
attrs.restore();
|
|
320
|
+
|
|
321
|
+
if (typeof el.setCustomValidity === 'function') el.setCustomValidity('');
|
|
301
322
|
}
|
|
302
323
|
};
|
|
303
324
|
});
|
|
@@ -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'));
|