griffincss-utils 0.16.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.
@@ -0,0 +1,609 @@
1
+ /*!
2
+ * Griffincss Utils — Runtime v0.16.0
3
+ * Достраивает то, чего статический CSS выразить не может.
4
+ * Пишется руками и не компилируется — правится этот файл.
5
+ */
6
+ (function (factory) {
7
+ 'use strict';
8
+
9
+ var api = factory();
10
+
11
+ if (typeof module === 'object' && module.exports) module.exports = api;
12
+
13
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
14
+ // Слияние, а не присваивание: ядро, тема и утилиты делят один глобал,
15
+ // и порядок тегов <script> не должен ничего значить. Своё
16
+ // пространство имён — по образцу Griffincss.theme: имена вроде
17
+ // observe и _autoStart есть и у ядра.
18
+ if (!window.Griffincss) window.Griffincss = {};
19
+
20
+ window.Griffincss.utils = api;
21
+ api._autoStart();
22
+ }
23
+ })(function () {
24
+ 'use strict';
25
+
26
+ var VERSION = '0.16.0';
27
+
28
+ // Таблица правил выводится из SCSS-карт скриптом scripts/sync-rule-table.mjs.
29
+ // Правьте карты в SCSS, а не этот блок: npm run sync -- --fix перепишет его.
30
+ var RULES = {
31
+ /* gr:rule-table */
32
+ 'gr-radius-': { base: 0.25, unit: 'rem', steps: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], literal: { full: '9999px' } },
33
+ 'gr-p-': { base: 0.25, unit: 'rem', steps: [0,1,2,3,4,5,6,7,8,10,12,16] }
34
+ /* /gr:rule-table */
35
+ };
36
+
37
+ // 'gr-radius-8' → '2rem'; 'gr-p-4' → '1rem'; 'gr-p-[13px]' → '13px';
38
+ // чужое → null.
39
+ function classValue(cls) {
40
+ if (cls.indexOf('[') !== -1) {
41
+ var custom = arbitraryOf(cls);
42
+
43
+ return custom ? custom.value : null;
44
+ }
45
+
46
+ for (var prefix in RULES) {
47
+ if (!Object.prototype.hasOwnProperty.call(RULES, prefix)) continue;
48
+ if (cls.indexOf(prefix) !== 0) continue;
49
+
50
+ var rest = cls.slice(prefix.length);
51
+ var rule = RULES[prefix];
52
+
53
+ if (rule.literal && Object.prototype.hasOwnProperty.call(rule.literal, rest)) {
54
+ return rule.literal[rest];
55
+ }
56
+
57
+ var step = Number(rest);
58
+
59
+ if (rest !== '' && !isNaN(step) && rule.steps.indexOf(step) !== -1) {
60
+ return step * rule.base + rule.unit;
61
+ }
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ // === Каскад скруглений ===
68
+ //
69
+ // Ничего не измеряет: getComputedStyle здесь нет вовсе. Собирает
70
+ // выражение из объявленных значений, арифметику делает CSS в calc().
71
+ // Статический CSS покрывает первый уровень и его прямых детей;
72
+ // достраивается только невыразимое — рекуррента r(n) = max(0, r(n−1) − p(n−1))
73
+ // для .gr-radius глубже первого.
74
+ //
75
+ // Состояния между вызовами рантайм не держит: действующий радиус
76
+ // предка пересчитывается по цепочке на месте. Поэтому проход
77
+ // идемпотентен, а перенос поддерева к другому предку даёт правильный
78
+ // ответ, а не подсказку из прошлой раскладки.
79
+
80
+ var LAYER = 'griffincss.utils';
81
+ var CONTAINER = 'gr-radius';
82
+ var DERIVED = /^gr-r-[0-9a-z]+$/;
83
+
84
+ // Пауза перед пересчётом после мутаций DOM — как в ядре.
85
+ var REFRESH_DELAY = 16;
86
+
87
+ var observers = []; // {root, observer, timer}
88
+ var parseObserver = null; // наблюдатель парсинга: живёт до DOMContentLoaded
89
+ var warned = {}; // текст предупреждения → уже сказано
90
+
91
+ // Одно и то же предупреждение говорится один раз: проход повторяется
92
+ // при каждой мутации, и без этого консоль заполнилась бы копиями.
93
+ function warn(message) {
94
+ if (Object.prototype.hasOwnProperty.call(warned, message)) return;
95
+
96
+ warned[message] = true;
97
+
98
+ if (typeof console !== 'undefined' && console.warn) console.warn('Griffincss: ' + message);
99
+ }
100
+
101
+ // Ядро: эмиттер, хеш и <style> живут в нём. Читается на месте, а не
102
+ // при загрузке, — порядок двух тегов <script> не должен ничего значить.
103
+ function core() {
104
+ var G = typeof window !== 'undefined' ? window.Griffincss : null;
105
+
106
+ if (G && typeof G._emit === 'function') return G;
107
+
108
+ warn('каскад скруглений требует griffincss.js — подключите ядро');
109
+
110
+ return null;
111
+ }
112
+
113
+ // Объявленное значение переменной: инлайн сильнее класса. Оба чтения —
114
+ // разбор атрибута: раскладка не трогается.
115
+ function declared(el, prop, prefix) {
116
+ var inline = el.style ? el.style.getPropertyValue(prop).trim() : '';
117
+ var fromClass = null;
118
+ var i;
119
+
120
+ for (i = 0; i < el.classList.length; i++) {
121
+ var cls = el.classList.item(i);
122
+ var value = classValue(cls);
123
+
124
+ if (value !== null && cls.indexOf(prefix) === 0) fromClass = value;
125
+ }
126
+
127
+ if (inline && fromClass && inline !== fromClass) {
128
+ warn(prop + ' задан и классом (' + fromClass + '), и в style (' + inline + ') — значения расходятся');
129
+ }
130
+
131
+ return inline || fromClass || null;
132
+ }
133
+
134
+ // Выражение радиуса для детей контейнера. Ничего не вычисляет —
135
+ // складывает строку, арифметику делает CSS.
136
+ function childRadius(radius, gap) {
137
+ if (!radius) return null;
138
+ if (!gap) return radius;
139
+
140
+ return 'max(0px,calc(' + radius + ' - ' + gap + '))';
141
+ }
142
+
143
+ function hasClass(node, name) {
144
+ return !!(node.classList && node.classList.contains(name));
145
+ }
146
+
147
+ // Действующие радиус и зазор ближайшего контейнера на уровне node или
148
+ // выше. Цепочка предков собирается подъёмом по parentNode, как в ядре,
149
+ // и разворачивается сверху вниз: радиус уровня — либо объявленный
150
+ // на нём самом, либо выведенный из предка.
151
+ function levelOf(node) {
152
+ var chain = [];
153
+ var cur = node;
154
+ var level = null;
155
+ var i;
156
+
157
+ while (cur && cur.classList) {
158
+ if (hasClass(cur, CONTAINER)) chain.push(cur);
159
+
160
+ cur = cur.parentNode;
161
+ }
162
+
163
+ for (i = chain.length - 1; i >= 0; i--) {
164
+ var own = declared(chain[i], '--gr-r', 'gr-radius-');
165
+
166
+ level = {
167
+ radius: own || (level ? childRadius(level.radius, level.gap) : null),
168
+ gap: declared(chain[i], '--gr-p', 'gr-p-')
169
+ };
170
+ }
171
+
172
+ return level;
173
+ }
174
+
175
+ // Один контейнер. Возвращает true, если в буфер ушло новое правило.
176
+ function apply(node) {
177
+ var G = core();
178
+
179
+ if (!G) return false;
180
+
181
+ var own = declared(node, '--gr-r', 'gr-radius-');
182
+
183
+ // Зазор читается и тогда, когда радиус свой и выводить нечего:
184
+ // расхождение класса и style — дефект разметки, и заметить его надо
185
+ // на любом контейнере, даже бездетном. Нужно здесь не значение,
186
+ // а сама проверка.
187
+ declared(node, '--gr-p', 'gr-p-');
188
+
189
+ var expr = own ? null : derivedRadius(node);
190
+ var cls = expr ? 'gr-r-' + G._hash(expr) : null;
191
+ var i;
192
+
193
+ // Прежние выведенные классы снимаются всегда: после переноса
194
+ // поддерева или появления явного радиуса старый класс остался бы
195
+ // на элементе и спорил бы с новым по порядку правил в таблице.
196
+ for (i = node.classList.length - 1; i >= 0; i--) {
197
+ var existing = node.classList.item(i);
198
+
199
+ if (DERIVED.test(existing) && existing !== cls) node.classList.remove(existing);
200
+ }
201
+
202
+ if (!cls) return false;
203
+
204
+ var fresh = G._emit(LAYER, expr, '.' + cls + '{--gr-r:' + expr + '}');
205
+
206
+ node.classList.add(cls);
207
+
208
+ return fresh;
209
+ }
210
+
211
+ // Радиус, выведенный из ближайшего предка-контейнера.
212
+ function derivedRadius(node) {
213
+ var above = levelOf(node.parentNode);
214
+
215
+ return above ? childRadius(above.radius, above.gap) : null;
216
+ }
217
+
218
+ // Проход по корню без записи в <style>: флаш делает вызывающий.
219
+ // Одна форма вложенности — один класс и одно правило: ключ буфера ядра
220
+ // и есть само выражение.
221
+ function scanRadius(root) {
222
+ var containers = (root || document).querySelectorAll('.' + CONTAINER);
223
+ var dirty = false;
224
+ var i;
225
+
226
+ for (i = 0; i < containers.length; i++) {
227
+ if (apply(containers[i])) dirty = true;
228
+ }
229
+
230
+ return dirty;
231
+ }
232
+
233
+ function radiusCascade(root) {
234
+ var G = core();
235
+
236
+ if (!G) return;
237
+
238
+ if (scanRadius(root)) G._flush();
239
+ }
240
+
241
+ // === Произвольные значения ===
242
+ //
243
+ // `.gr-mt-[13px]` — то, чего статический файл выразить не может:
244
+ // список значений неизвестен до того, как страница написана.
245
+ // Правило собирается в рантайме и уезжает в тот же слой утилит,
246
+ // поэтому спорит с остальными классами по порядку, а не по весу.
247
+ //
248
+ // Таблица повторяет объявления статического класса один в один:
249
+ // `.gr-p-4` пишет и `--gr-p`, и `padding`, и произвольный вариант
250
+ // обязан писать столько же — иначе каскад скруглений увидит зазор
251
+ // у одного класса и не увидит у другого. Сверяется тестом
252
+ // arbitrary.test.js по собранному CSS.
253
+ //
254
+ // Свойств с двумя смыслами в таблице нет: `.gr-text-2xl` — кегль,
255
+ // а `.gr-text-white` — цвет, и `.gr-text-[13px]` пришлось бы угадывать.
256
+ // Угадывать рантайм не будет.
257
+ //
258
+ // Запись единая — `свойство:{}` в каждой строке, даже там, где
259
+ // объявление одно. Короткая форма замерена и отвергнута: gzip
260
+ // повторение съедает сам, и выигрыш в 11 Б не стоит второй формы
261
+ // записи в таблице.
262
+ var PROPS = {
263
+ 'gr-m-': 'margin:{}',
264
+ 'gr-mt-': 'margin-top:{}',
265
+ 'gr-mr-': 'margin-right:{}',
266
+ 'gr-mb-': 'margin-bottom:{}',
267
+ 'gr-ml-': 'margin-left:{}',
268
+ 'gr-ms-': 'margin-inline-start:{}',
269
+ 'gr-me-': 'margin-inline-end:{}',
270
+ 'gr-mx-': 'margin-inline:{}',
271
+ 'gr-my-': 'margin-top:{};margin-bottom:{}',
272
+ 'gr-p-': '--gr-p:{};padding:var(--gr-p)',
273
+ 'gr-pt-': 'padding-top:{}',
274
+ 'gr-pr-': 'padding-right:{}',
275
+ 'gr-pb-': 'padding-bottom:{}',
276
+ 'gr-pl-': 'padding-left:{}',
277
+ 'gr-ps-': 'padding-inline-start:{}',
278
+ 'gr-pe-': 'padding-inline-end:{}',
279
+ 'gr-px-': 'padding-inline:{}',
280
+ 'gr-py-': 'padding-top:{};padding-bottom:{}',
281
+ 'gr-radius-': '--gr-r:{};border-radius:{}',
282
+ 'gr-gap-': 'gap:{}',
283
+ 'gr-w-': 'width:{}',
284
+ 'gr-h-': 'height:{}',
285
+ 'gr-min-w-': 'min-width:{}',
286
+ 'gr-max-w-': 'max-width:{}',
287
+ 'gr-min-h-': 'min-height:{}',
288
+ 'gr-max-h-': 'max-height:{}',
289
+ 'gr-top-': 'top:{}',
290
+ 'gr-right-': 'right:{}',
291
+ 'gr-bottom-': 'bottom:{}',
292
+ 'gr-left-': 'left:{}',
293
+ 'gr-inset-': 'inset:{}',
294
+ 'gr-z-': 'z-index:{}'
295
+ };
296
+
297
+ // Содержимое скобок — ввод из разметки, и проверяется он так же, как
298
+ // строка раскладки в ядре: длина и запрет на то, чем правило можно
299
+ // закрыть досрочно. Пробела в имени класса не бывает по определению,
300
+ // но проверяется и он: classValue вызывают и напрямую.
301
+ var MAX_VALUE_LENGTH = 48;
302
+ var BAD_VALUE = /[{};]|\/\*|\s/;
303
+
304
+ // Элементы с произвольным значением: скобка в имени класса.
305
+ var ARBITRARY_SELECTOR = '[class*="["]';
306
+
307
+ // Самый длинный подходящий префикс: короткий не должен перехватывать
308
+ // класс длинного, как и в таблице правил.
309
+ function propFor(name) {
310
+ var best = null;
311
+ var prefix;
312
+
313
+ for (prefix in PROPS) {
314
+ if (!Object.prototype.hasOwnProperty.call(PROPS, prefix)) continue;
315
+ if (name.indexOf(prefix) !== 0) continue;
316
+ if (!best || prefix.length > best.length) best = prefix;
317
+ }
318
+
319
+ return best;
320
+ }
321
+
322
+ // 'gr-mt-[13px]' → { prefix: 'gr-mt-', value: '13px' }.
323
+ // Невалидное — предупреждение и null: пропуск одного класса дешевле
324
+ // сломанного правила, за которым в таблице стилей идут остальные.
325
+ function arbitraryOf(cls) {
326
+ var open = cls.indexOf('[');
327
+
328
+ if (open === -1 || cls.charAt(cls.length - 1) !== ']') return null;
329
+
330
+ var value = cls.slice(open + 1, cls.length - 1);
331
+
332
+ if (value === '' || value.length > MAX_VALUE_LENGTH || BAD_VALUE.test(value)) {
333
+ warn('произвольное значение в .' + cls + ' отклонено');
334
+
335
+ return null;
336
+ }
337
+
338
+ var prefix = propFor(cls.slice(0, open));
339
+
340
+ if (!prefix) {
341
+ warn('произвольное значение .' + cls + ' — свойство неизвестно');
342
+
343
+ return null;
344
+ }
345
+
346
+ return { prefix: prefix, value: value };
347
+ }
348
+
349
+ // Экранирование обязательно: без него `.gr-mt-[13px]` читается как
350
+ // селектор класса `gr-mt-` с последующим селектором по атрибуту,
351
+ // и правило не сматчится ни с чем.
352
+ function escapeClass(cls) {
353
+ return cls.replace(/[^\w-]/g, function (ch) {
354
+ return '\\' + ch;
355
+ });
356
+ }
357
+
358
+ function arbitraryRule(cls) {
359
+ var parsed = arbitraryOf(cls);
360
+
361
+ if (!parsed) return null;
362
+
363
+ return '.' + escapeClass(cls) + '{' + PROPS[parsed.prefix].split('{}').join(parsed.value) + '}';
364
+ }
365
+
366
+ // Один элемент. Ключ буфера — само имя класса: одинаковые значения
367
+ // на разных элементах дают одно правило.
368
+ function applyArbitrary(node) {
369
+ var G = core();
370
+ var fresh = false;
371
+ var i;
372
+
373
+ if (!G || !node.classList) return false;
374
+
375
+ for (i = 0; i < node.classList.length; i++) {
376
+ var cls = node.classList.item(i);
377
+
378
+ if (cls.indexOf('[') === -1) continue;
379
+
380
+ var rule = arbitraryRule(cls);
381
+
382
+ if (rule && G._emit(LAYER, cls, rule)) fresh = true;
383
+ }
384
+
385
+ return fresh;
386
+ }
387
+
388
+ // Проход по корню без записи в <style>: флаш делает вызывающий.
389
+ function scanArbitrary(root) {
390
+ root = root || document;
391
+
392
+ var nodes = root.querySelectorAll(ARBITRARY_SELECTOR);
393
+ var dirty = applyArbitrary(root);
394
+ var i;
395
+
396
+ for (i = 0; i < nodes.length; i++) {
397
+ if (applyArbitrary(nodes[i])) dirty = true;
398
+ }
399
+
400
+ return dirty;
401
+ }
402
+
403
+ function arbitrary(root) {
404
+ var G = core();
405
+
406
+ if (!G) return;
407
+
408
+ if (scanArbitrary(root)) G._flush();
409
+ }
410
+
411
+ // Оба прохода разом. Флаш один на два: таблица переписывается целиком,
412
+ // и делать это дважды подряд незачем.
413
+ function sweep(root) {
414
+ var G = core();
415
+
416
+ if (!G) return;
417
+
418
+ var dirty = scanArbitrary(root);
419
+
420
+ if (scanRadius(root)) dirty = true;
421
+
422
+ if (dirty) G._flush();
423
+ }
424
+
425
+ // Добавленные узлы: сам узел и всё, что внутри него.
426
+ function handleAdded(nodes) {
427
+ var dirty = false;
428
+ var i;
429
+ var j;
430
+
431
+ for (i = 0; i < nodes.length; i++) {
432
+ var node = nodes[i];
433
+
434
+ if (!node || node.nodeType !== 1) continue;
435
+
436
+ if (applyArbitrary(node)) dirty = true;
437
+
438
+ if (hasClass(node, CONTAINER) && apply(node)) dirty = true;
439
+
440
+ if (!node.querySelectorAll) continue;
441
+
442
+ var custom = node.querySelectorAll(ARBITRARY_SELECTOR);
443
+
444
+ for (j = 0; j < custom.length; j++) {
445
+ if (applyArbitrary(custom[j])) dirty = true;
446
+ }
447
+
448
+ var inner = node.querySelectorAll('.' + CONTAINER);
449
+
450
+ for (j = 0; j < inner.length; j++) {
451
+ if (apply(inner[j])) dirty = true;
452
+ }
453
+ }
454
+
455
+ return dirty;
456
+ }
457
+
458
+ // Есть ли в порции добавленных узлов работа: контейнер скруглений или
459
+ // произвольное значение. Без этой проверки дебаунс срабатывал бы на любой
460
+ // вставке текста.
461
+ function hasWork(mutations) {
462
+ var i;
463
+
464
+ for (i = 0; i < mutations.length; i++) {
465
+ var nodes = mutations[i].addedNodes;
466
+ var j;
467
+
468
+ for (j = 0; j < nodes.length; j++) {
469
+ var node = nodes[j];
470
+
471
+ if (!node || node.nodeType !== 1) continue;
472
+ if (hasClass(node, CONTAINER)) return true;
473
+ if (node.className && node.className.indexOf('[') !== -1) return true;
474
+ if (!node.querySelectorAll) continue;
475
+ if (node.querySelectorAll('.' + CONTAINER).length > 0) return true;
476
+ if (node.querySelectorAll(ARBITRARY_SELECTOR).length > 0) return true;
477
+ }
478
+ }
479
+
480
+ return false;
481
+ }
482
+
483
+ // Наблюдение за парсингом документа: контейнеры получают радиус по мере
484
+ // появления, а не разом на DOMContentLoaded. Дебаунса здесь нет
485
+ // намеренно — он и есть та задержка, которой мы избегаем.
486
+ function observeParsing() {
487
+ if (parseObserver || typeof MutationObserver === 'undefined') return;
488
+
489
+ parseObserver = new MutationObserver(function (mutations) {
490
+ var dirty = false;
491
+ var i;
492
+
493
+ for (i = 0; i < mutations.length; i++) {
494
+ if (handleAdded(mutations[i].addedNodes)) dirty = true;
495
+ }
496
+
497
+ // Флаш один на порцию, а не на элемент: таблица переписывается целиком.
498
+ if (dirty) core()._flush();
499
+ });
500
+
501
+ parseObserver.observe(document.documentElement, { childList: true, subtree: true });
502
+ }
503
+
504
+ function stopParseObserver() {
505
+ if (!parseObserver) return;
506
+
507
+ parseObserver.disconnect();
508
+ parseObserver = null;
509
+ }
510
+
511
+ // Наблюдение за живым поддеревом: пересчёт с дебаунсом и только по
512
+ // этому корню. Пока страница разбирается, работает поток, дальше —
513
+ // этот наблюдатель.
514
+ function observe(root) {
515
+ if (typeof MutationObserver === 'undefined') return;
516
+
517
+ root = root || document.body;
518
+
519
+ var i;
520
+
521
+ for (i = 0; i < observers.length; i++) {
522
+ if (observers[i].root === root) return;
523
+ }
524
+
525
+ var entry = { root: root, observer: null, timer: null };
526
+
527
+ entry.observer = new MutationObserver(function (mutations) {
528
+ if (!hasWork(mutations)) return;
529
+
530
+ if (entry.timer) clearTimeout(entry.timer);
531
+
532
+ entry.timer = setTimeout(function () {
533
+ entry.timer = null;
534
+ sweep(entry.root);
535
+ }, REFRESH_DELAY);
536
+ });
537
+
538
+ entry.observer.observe(root, { childList: true, subtree: true });
539
+ observers.push(entry);
540
+ }
541
+
542
+ // Снять всё наблюдение. Выданные классы и правила остаются: они
543
+ // описывают текущее дерево и снимаются только сменой разметки.
544
+ function stop() {
545
+ var i;
546
+
547
+ stopParseObserver();
548
+
549
+ for (i = 0; i < observers.length; i++) {
550
+ if (observers[i].timer) clearTimeout(observers[i].timer);
551
+
552
+ observers[i].observer.disconnect();
553
+ }
554
+
555
+ observers = [];
556
+ }
557
+
558
+ // Автостарт: только в браузере.
559
+ // data-auto="false" — не запускаться вовсе;
560
+ // data-stream="false" — без потокового режима, один проход в конце;
561
+ // data-observe="false" — без наблюдения за живым деревом.
562
+ function autoStart() {
563
+ var auto = true;
564
+ var streaming = true;
565
+ var watching = true;
566
+
567
+ try {
568
+ var script = document.currentScript;
569
+
570
+ if (script) {
571
+ if (script.getAttribute('data-auto') === 'false') auto = false;
572
+ if (script.getAttribute('data-stream') === 'false') streaming = false;
573
+ if (script.getAttribute('data-observe') === 'false') watching = false;
574
+ }
575
+ } catch (e) { /* currentScript недоступен */ }
576
+
577
+ if (!auto) return;
578
+
579
+ function settle() {
580
+ stopParseObserver();
581
+ sweep(document);
582
+
583
+ // Наблюдение за живым деревом включено по умолчанию, в отличие от
584
+ // ядра: раскладку скругление не двигает, зато вставленная позже
585
+ // карточка без него осталась бы с квадратными углами молча.
586
+ if (watching) observe(document.body);
587
+ }
588
+
589
+ if (document.readyState === 'loading') {
590
+ if (streaming) observeParsing();
591
+
592
+ document.addEventListener('DOMContentLoaded', settle);
593
+ } else {
594
+ settle();
595
+ }
596
+ }
597
+
598
+ return {
599
+ radiusCascade: radiusCascade,
600
+ arbitrary: arbitrary,
601
+ observe: observe,
602
+ stop: stop,
603
+ classValue: classValue,
604
+ version: VERSION,
605
+ _rules: RULES,
606
+ _props: PROPS,
607
+ _autoStart: autoStart
608
+ };
609
+ });