@react-x11/components 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/css/animation.d.ts +91 -0
- package/dist/html/css/animation.d.ts.map +1 -0
- package/dist/html/css/animation.js +430 -0
- package/dist/html/css/animation.js.map +1 -0
- package/dist/html/css/cascade.d.ts +27 -4
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +94 -6
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +35 -0
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +115 -2
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +5 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +57 -7
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +139 -80
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +8 -0
- package/dist/html/node.js.map +1 -1
- package/package.json +1 -1
- package/src/html/css/animation.ts +466 -0
- package/src/html/css/cascade.ts +130 -7
- package/src/html/css/parse.ts +151 -2
- package/src/html/css/style.ts +69 -7
- package/src/html/layout/flex.ts +146 -83
- package/src/html/node.ts +8 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
// `animation` and its longhands (CSS Animations 1, 4), read into the lists
|
|
2
|
+
// an element's animations are, and what those animations leave on its
|
|
3
|
+
// style.
|
|
4
|
+
//
|
|
5
|
+
// Nothing here runs an animation: a document is drawn as it stands once
|
|
6
|
+
// each of its animations has run one iteration at no length. One that
|
|
7
|
+
// fills forwards then holds the frame it ended on, and any other leaves
|
|
8
|
+
// the style it started from. That is the moment the Zen Garden bench holds
|
|
9
|
+
// Chrome at (`scripts/zengarden/chrome.ts`), and the one a page that fades
|
|
10
|
+
// its panels in and holds them is meant to be seen at: drawn as it starts,
|
|
11
|
+
// a panel that animates in from nothing is never there. So the cascade asks
|
|
12
|
+
// `restingFrames` for the declarations of each such frame and applies them
|
|
13
|
+
// above the author's own, as the animation origin is (CSS Cascade 5, 6.1).
|
|
14
|
+
|
|
15
|
+
import { parseMath } from './calc.js';
|
|
16
|
+
import type { Declaration, KeyframesRule } from './parse.js';
|
|
17
|
+
import { parseNumber, splitCommas, splitValue } from './values.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An easing function (CSS Easing 2), as an animation's timing function or
|
|
21
|
+
* a keyframe's: the identity, a cubic Bézier, steps, or the points of a
|
|
22
|
+
* `linear()` with every input placed.
|
|
23
|
+
*/
|
|
24
|
+
export type Easing =
|
|
25
|
+
| { type: 'linear' }
|
|
26
|
+
| { type: 'cubic'; x1: number; y1: number; x2: number; y2: number }
|
|
27
|
+
| { type: 'steps'; count: number; position: StepPosition }
|
|
28
|
+
| { type: 'points'; points: readonly { input: number; output: number }[] };
|
|
29
|
+
|
|
30
|
+
export type StepPosition =
|
|
31
|
+
'jump-start' | 'jump-end' | 'jump-none' | 'jump-both';
|
|
32
|
+
|
|
33
|
+
export type AnimationDirection =
|
|
34
|
+
'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
|
35
|
+
|
|
36
|
+
export type FillMode = 'none' | 'forwards' | 'backwards' | 'both';
|
|
37
|
+
|
|
38
|
+
export type PlayState = 'running' | 'paused';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An element's animations: each longhand's list as computed. There are as
|
|
42
|
+
* many animations as `names` has entries; a shorter list of another
|
|
43
|
+
* longhand repeats, and a longer one is cut (CSS Animations 1, 4).
|
|
44
|
+
*/
|
|
45
|
+
export interface Animations {
|
|
46
|
+
/** `animation-name`: the `@keyframes` each runs, null for `none`. */
|
|
47
|
+
readonly names: readonly (string | null)[];
|
|
48
|
+
/** `animation-duration`, in milliseconds: `auto` is 0. */
|
|
49
|
+
readonly durations: readonly number[];
|
|
50
|
+
/** `animation-timing-function`. */
|
|
51
|
+
readonly easings: readonly Easing[];
|
|
52
|
+
/** `animation-delay`, in milliseconds, before the first iteration. */
|
|
53
|
+
readonly delays: readonly number[];
|
|
54
|
+
/** `animation-iteration-count`: `infinite` is `Infinity`. */
|
|
55
|
+
readonly iterations: readonly number[];
|
|
56
|
+
readonly directions: readonly AnimationDirection[];
|
|
57
|
+
readonly fillModes: readonly FillMode[];
|
|
58
|
+
readonly playStates: readonly PlayState[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const LINEAR: Easing = { type: 'linear' };
|
|
62
|
+
const EASE: Easing = { type: 'cubic', x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 };
|
|
63
|
+
|
|
64
|
+
/** The easing keywords (CSS Easing 2, 2.2 and 3.1). */
|
|
65
|
+
const EASINGS: Record<string, Easing> = {
|
|
66
|
+
linear: LINEAR,
|
|
67
|
+
ease: EASE,
|
|
68
|
+
'ease-in': { type: 'cubic', x1: 0.42, y1: 0, x2: 1, y2: 1 },
|
|
69
|
+
'ease-out': { type: 'cubic', x1: 0, y1: 0, x2: 0.58, y2: 1 },
|
|
70
|
+
'ease-in-out': { type: 'cubic', x1: 0.42, y1: 0, x2: 0.58, y2: 1 },
|
|
71
|
+
'step-start': { type: 'steps', count: 1, position: 'jump-start' },
|
|
72
|
+
'step-end': { type: 'steps', count: 1, position: 'jump-end' },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** The initial value of every longhand: one animation, of nothing. Every
|
|
76
|
+
* element without one shares it, which is how the cascade tells. */
|
|
77
|
+
export const NO_ANIMATIONS: Animations = {
|
|
78
|
+
names: [null],
|
|
79
|
+
durations: [0],
|
|
80
|
+
easings: [EASE],
|
|
81
|
+
delays: [0],
|
|
82
|
+
iterations: [1],
|
|
83
|
+
directions: ['normal'],
|
|
84
|
+
fillModes: ['none'],
|
|
85
|
+
playStates: ['running'],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** The longhands, by property name, and the list each is. */
|
|
89
|
+
export const ANIMATION_LONGHANDS: Record<string, keyof Animations> = {
|
|
90
|
+
'animation-name': 'names',
|
|
91
|
+
'animation-duration': 'durations',
|
|
92
|
+
'animation-timing-function': 'easings',
|
|
93
|
+
'animation-delay': 'delays',
|
|
94
|
+
'animation-iteration-count': 'iterations',
|
|
95
|
+
'animation-direction': 'directions',
|
|
96
|
+
'animation-fill-mode': 'fillModes',
|
|
97
|
+
'animation-play-state': 'playStates',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const DIRECTIONS = new Set<string>([
|
|
101
|
+
'normal',
|
|
102
|
+
'reverse',
|
|
103
|
+
'alternate',
|
|
104
|
+
'alternate-reverse',
|
|
105
|
+
]);
|
|
106
|
+
const FILL_MODES = new Set<string>(['none', 'forwards', 'backwards', 'both']);
|
|
107
|
+
const PLAY_STATES = new Set<string>(['running', 'paused']);
|
|
108
|
+
|
|
109
|
+
/** One value of each longhand's list; undefined where it is none, and so
|
|
110
|
+
* is null but for a name, where it is `none`. */
|
|
111
|
+
const ONE: {
|
|
112
|
+
[K in keyof Animations]: (
|
|
113
|
+
value: string,
|
|
114
|
+
) => Animations[K][number] | null | undefined;
|
|
115
|
+
} = {
|
|
116
|
+
names: animationName,
|
|
117
|
+
durations: (v) => (v.toLowerCase() === 'auto' ? 0 : duration(v)),
|
|
118
|
+
easings: parseEasing,
|
|
119
|
+
delays: parseTime,
|
|
120
|
+
iterations: iterationCount,
|
|
121
|
+
directions: (v) => keyword(v, DIRECTIONS) as AnimationDirection | null,
|
|
122
|
+
fillModes: (v) => keyword(v, FILL_MODES) as FillMode | null,
|
|
123
|
+
playStates: (v) => keyword(v, PLAY_STATES) as PlayState | null,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `animations` with one longhand's list set from a declaration's value, or
|
|
128
|
+
* null where the value is not a list of that longhand's values and the
|
|
129
|
+
* declaration is dropped.
|
|
130
|
+
*/
|
|
131
|
+
export function animationLonghand(
|
|
132
|
+
animations: Animations,
|
|
133
|
+
key: keyof Animations,
|
|
134
|
+
value: string,
|
|
135
|
+
): Animations | null {
|
|
136
|
+
const read = ONE[key] as (value: string) => unknown;
|
|
137
|
+
const list: unknown[] = [];
|
|
138
|
+
for (const item of splitCommas(value)) {
|
|
139
|
+
const one = read(item.trim());
|
|
140
|
+
if (one === undefined || (one === null && key !== 'names')) return null;
|
|
141
|
+
list.push(one);
|
|
142
|
+
}
|
|
143
|
+
return { ...animations, [key]: list };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The `animation` shorthand: a comma list of animations, each its
|
|
148
|
+
* longhands' values in any order and the rest at their initial values — or
|
|
149
|
+
* null where an item is not one. The first time is the duration and the
|
|
150
|
+
* second the delay; a keyword goes to the first longhand it is a value of
|
|
151
|
+
* that the item has not set, and only then is it the name, so that
|
|
152
|
+
* `animation: none` is a fill mode and `animation: 1s forwards slide` names
|
|
153
|
+
* `slide` (CSS Animations 1, 4.9).
|
|
154
|
+
*/
|
|
155
|
+
export function parseAnimation(value: string): Animations | null {
|
|
156
|
+
const names: (string | null)[] = [];
|
|
157
|
+
const durations: number[] = [];
|
|
158
|
+
const easings: Easing[] = [];
|
|
159
|
+
const delays: number[] = [];
|
|
160
|
+
const iterations: number[] = [];
|
|
161
|
+
const directions: AnimationDirection[] = [];
|
|
162
|
+
const fillModes: FillMode[] = [];
|
|
163
|
+
const playStates: PlayState[] = [];
|
|
164
|
+
for (const item of splitCommas(value)) {
|
|
165
|
+
const tokens = splitValue(item.trim());
|
|
166
|
+
if (!tokens.length) return null;
|
|
167
|
+
let name: string | null | undefined;
|
|
168
|
+
let time = 0;
|
|
169
|
+
let dur: number | undefined;
|
|
170
|
+
let del: number | undefined;
|
|
171
|
+
let easing: Easing | undefined;
|
|
172
|
+
let count: number | undefined;
|
|
173
|
+
let direction: AnimationDirection | undefined;
|
|
174
|
+
let fill: FillMode | undefined;
|
|
175
|
+
let play: PlayState | undefined;
|
|
176
|
+
for (const token of tokens) {
|
|
177
|
+
const lower = token.toLowerCase();
|
|
178
|
+
const t = parseTime(token);
|
|
179
|
+
if (t !== null) {
|
|
180
|
+
if (time === 0) {
|
|
181
|
+
if (t < 0) return null;
|
|
182
|
+
dur = t;
|
|
183
|
+
} else if (time === 1) {
|
|
184
|
+
del = t;
|
|
185
|
+
} else {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
time += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (easing === undefined) {
|
|
192
|
+
const e = parseEasing(token);
|
|
193
|
+
if (e !== null) {
|
|
194
|
+
easing = e;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (count === undefined) {
|
|
199
|
+
const n = iterationCount(token);
|
|
200
|
+
if (n !== null) {
|
|
201
|
+
count = n;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (direction === undefined && DIRECTIONS.has(lower)) {
|
|
206
|
+
direction = lower as AnimationDirection;
|
|
207
|
+
} else if (fill === undefined && FILL_MODES.has(lower)) {
|
|
208
|
+
fill = lower as FillMode;
|
|
209
|
+
} else if (play === undefined && PLAY_STATES.has(lower)) {
|
|
210
|
+
play = lower as PlayState;
|
|
211
|
+
} else if (name === undefined) {
|
|
212
|
+
const n = animationName(token);
|
|
213
|
+
if (n === undefined) return null;
|
|
214
|
+
name = n;
|
|
215
|
+
} else {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
names.push(name ?? null);
|
|
220
|
+
durations.push(dur ?? 0);
|
|
221
|
+
easings.push(easing ?? EASE);
|
|
222
|
+
delays.push(del ?? 0);
|
|
223
|
+
iterations.push(count ?? 1);
|
|
224
|
+
directions.push(direction ?? 'normal');
|
|
225
|
+
fillModes.push(fill ?? 'none');
|
|
226
|
+
playStates.push(play ?? 'running');
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
names,
|
|
230
|
+
durations,
|
|
231
|
+
easings,
|
|
232
|
+
delays,
|
|
233
|
+
iterations,
|
|
234
|
+
directions,
|
|
235
|
+
fillModes,
|
|
236
|
+
playStates,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** An `animation-name` entry: the name, null for `none`, and undefined for
|
|
241
|
+
* what is neither — a CSS-wide keyword, `default`, or no identifier. */
|
|
242
|
+
function animationName(value: string): string | null | undefined {
|
|
243
|
+
const v = value.trim();
|
|
244
|
+
if (v[0] === '"' || v[0] === "'") {
|
|
245
|
+
const q = v[0];
|
|
246
|
+
return v.length >= 2 && v[v.length - 1] === q ? v.slice(1, -1) : undefined;
|
|
247
|
+
}
|
|
248
|
+
const lower = v.toLowerCase();
|
|
249
|
+
if (lower === 'none') return null;
|
|
250
|
+
if (!IDENT.test(v) || RESERVED.has(lower)) return undefined;
|
|
251
|
+
return v;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const IDENT = /^-?(?:[_a-zA-Z\u0080-]|--)[\w\u0080--]*$/;
|
|
255
|
+
|
|
256
|
+
const RESERVED = new Set([
|
|
257
|
+
'inherit',
|
|
258
|
+
'initial',
|
|
259
|
+
'unset',
|
|
260
|
+
'revert',
|
|
261
|
+
'revert-layer',
|
|
262
|
+
'default',
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
/** A `<time>` in milliseconds: `2s`, `150ms`, a `calc()` of them; null
|
|
266
|
+
* for anything else. */
|
|
267
|
+
export function parseTime(value: string): number | null {
|
|
268
|
+
const v = value.trim().toLowerCase();
|
|
269
|
+
const m = TIME.exec(v);
|
|
270
|
+
if (m) return Number(m[1]) * (m[2] === 's' ? 1000 : 1);
|
|
271
|
+
if (!v.includes('(')) return null;
|
|
272
|
+
const sum = parseMath(v, (token) => {
|
|
273
|
+
const unit = TIME.exec(token);
|
|
274
|
+
return unit ? Number(unit[1]) * (unit[2] === 's' ? 1000 : 1) : null;
|
|
275
|
+
});
|
|
276
|
+
return typeof sum === 'number' ? sum : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const TIME = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(s|ms)$/;
|
|
280
|
+
|
|
281
|
+
/** A duration: a time that is not negative. */
|
|
282
|
+
function duration(value: string): number | null {
|
|
283
|
+
const t = parseTime(value);
|
|
284
|
+
return t !== null && t >= 0 ? t : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** `infinite`, or a number that is not negative. */
|
|
288
|
+
function iterationCount(value: string): number | null {
|
|
289
|
+
const v = value.trim().toLowerCase();
|
|
290
|
+
if (v === 'infinite') return Infinity;
|
|
291
|
+
const n = parseNumber(v);
|
|
292
|
+
return n !== null && n >= 0 ? n : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function keyword(value: string, set: ReadonlySet<string>): string | null {
|
|
296
|
+
const v = value.trim().toLowerCase();
|
|
297
|
+
return set.has(v) ? v : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* An easing function (CSS Easing 2): a keyword, `cubic-bezier()`,
|
|
302
|
+
* `steps()` or `linear()`; null for anything else.
|
|
303
|
+
*/
|
|
304
|
+
export function parseEasing(value: string): Easing | null {
|
|
305
|
+
const v = value.trim().toLowerCase();
|
|
306
|
+
const named = EASINGS[v];
|
|
307
|
+
if (named) return named;
|
|
308
|
+
const m = /^(cubic-bezier|steps|linear)\((.*)\)$/s.exec(v);
|
|
309
|
+
if (!m) return null;
|
|
310
|
+
const args = splitCommas(m[2]);
|
|
311
|
+
if (m[1] === 'cubic-bezier') {
|
|
312
|
+
if (args.length !== 4) return null;
|
|
313
|
+
const [x1, y1, x2, y2] = args.map(parseNumber);
|
|
314
|
+
if (x1 === null || y1 === null || x2 === null || y2 === null) return null;
|
|
315
|
+
// the x of each control point is a time, inside the interval
|
|
316
|
+
if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) return null;
|
|
317
|
+
return { type: 'cubic', x1, y1, x2, y2 };
|
|
318
|
+
}
|
|
319
|
+
if (m[1] === 'steps') {
|
|
320
|
+
if (args.length < 1 || args.length > 2) return null;
|
|
321
|
+
const count = parseNumber(args[0]);
|
|
322
|
+
if (count === null || !Number.isInteger(count)) return null;
|
|
323
|
+
const position = stepPosition(args[1]);
|
|
324
|
+
if (!position) return null;
|
|
325
|
+
if (count < (position === 'jump-none' ? 2 : 1)) return null;
|
|
326
|
+
return { type: 'steps', count, position };
|
|
327
|
+
}
|
|
328
|
+
return linearPoints(args);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function stepPosition(value: string | undefined): StepPosition | null {
|
|
332
|
+
const v = value?.trim() ?? 'jump-end';
|
|
333
|
+
if (v === 'start') return 'jump-start';
|
|
334
|
+
if (v === 'end') return 'jump-end';
|
|
335
|
+
return v === 'jump-start' ||
|
|
336
|
+
v === 'jump-end' ||
|
|
337
|
+
v === 'jump-none' ||
|
|
338
|
+
v === 'jump-both'
|
|
339
|
+
? v
|
|
340
|
+
: null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* `linear()`'s stops as points (CSS Easing 2, 2.1.1): each an output with
|
|
345
|
+
* up to two input percentages, two making two points. The first point
|
|
346
|
+
* with no input is at 0 and the last at 1, an input less than one before
|
|
347
|
+
* it is that one, and the points between two that have one are spread
|
|
348
|
+
* evenly between them.
|
|
349
|
+
*/
|
|
350
|
+
function linearPoints(args: string[]): Easing | null {
|
|
351
|
+
const points: { input: number | null; output: number }[] = [];
|
|
352
|
+
for (const arg of args) {
|
|
353
|
+
const parts = splitValue(arg.trim());
|
|
354
|
+
let output: number | null = null;
|
|
355
|
+
const inputs: number[] = [];
|
|
356
|
+
for (const part of parts) {
|
|
357
|
+
if (part.endsWith('%')) {
|
|
358
|
+
const pct = parseNumber(part.slice(0, -1));
|
|
359
|
+
if (pct === null || inputs.length === 2) return null;
|
|
360
|
+
inputs.push(pct / 100);
|
|
361
|
+
} else {
|
|
362
|
+
if (output !== null) return null;
|
|
363
|
+
output = parseNumber(part);
|
|
364
|
+
if (output === null) return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (output === null) return null;
|
|
368
|
+
if (!inputs.length) points.push({ input: null, output });
|
|
369
|
+
for (const input of inputs) points.push({ input, output });
|
|
370
|
+
}
|
|
371
|
+
if (points.length < 2) return null;
|
|
372
|
+
if (points[0].input === null) points[0].input = 0;
|
|
373
|
+
const last = points[points.length - 1];
|
|
374
|
+
if (last.input === null) last.input = Math.max(1, highest(points));
|
|
375
|
+
let largest = -Infinity;
|
|
376
|
+
for (const point of points) {
|
|
377
|
+
if (point.input === null) continue;
|
|
378
|
+
if (point.input < largest) point.input = largest;
|
|
379
|
+
largest = point.input;
|
|
380
|
+
}
|
|
381
|
+
for (let i = 1; i < points.length;) {
|
|
382
|
+
if (points[i].input !== null) {
|
|
383
|
+
i += 1;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
let j = i + 1;
|
|
387
|
+
while (points[j].input === null) j += 1;
|
|
388
|
+
const from = points[i - 1].input!;
|
|
389
|
+
const to = points[j].input!;
|
|
390
|
+
for (let k = i; k < j; k += 1) {
|
|
391
|
+
points[k].input = from + ((to - from) * (k - i + 1)) / (j - i + 1);
|
|
392
|
+
}
|
|
393
|
+
i = j + 1;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
type: 'points',
|
|
397
|
+
points: points as { input: number; output: number }[],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The largest input placed so far. */
|
|
402
|
+
function highest(points: { input: number | null }[]): number {
|
|
403
|
+
let out = -Infinity;
|
|
404
|
+
for (const point of points) {
|
|
405
|
+
if (point.input !== null && point.input > out) out = point.input;
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The declarations an element's animations leave on its style once each
|
|
412
|
+
* has run one iteration at no length, one list an animation, in the order
|
|
413
|
+
* they are named — a later one over an earlier where both set a property
|
|
414
|
+
* (CSS Animations 1, 4.2). An animation leaves the frame it ends on where
|
|
415
|
+
* it fills forwards: the `to` frame, or the `from` frame where it plays in
|
|
416
|
+
* reverse, merged where several frames are at that offset, a later one's
|
|
417
|
+
* declaration over an earlier one's. A property the frame does not set is
|
|
418
|
+
* the element's own, which there is nothing to say about. Null where no
|
|
419
|
+
* animation leaves anything.
|
|
420
|
+
*/
|
|
421
|
+
export function restingFrames(
|
|
422
|
+
animations: Animations,
|
|
423
|
+
keyframesOf: (name: string) => KeyframesRule | null,
|
|
424
|
+
): Declaration[][] | null {
|
|
425
|
+
let out: Declaration[][] | null = null;
|
|
426
|
+
const { names, directions, fillModes } = animations;
|
|
427
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
428
|
+
const name = names[i];
|
|
429
|
+
if (name === null) continue;
|
|
430
|
+
const fill = fillModes[i % fillModes.length];
|
|
431
|
+
if (fill !== 'forwards' && fill !== 'both') continue;
|
|
432
|
+
const rule = keyframesOf(name);
|
|
433
|
+
if (!rule) continue;
|
|
434
|
+
const direction = directions[i % directions.length];
|
|
435
|
+
const reversed =
|
|
436
|
+
direction === 'reverse' || direction === 'alternate-reverse';
|
|
437
|
+
const frame = endFrame(rule, reversed ? 0 : 1);
|
|
438
|
+
if (frame.length) (out ??= []).push(frame);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The declarations at one offset of a `@keyframes`, in the order written:
|
|
444
|
+
* one array per rule and offset, so the cascade sees the same one each
|
|
445
|
+
* time it asks. */
|
|
446
|
+
function endFrame(rule: KeyframesRule, offset: 0 | 1): Declaration[] {
|
|
447
|
+
let ends = END_FRAMES.get(rule);
|
|
448
|
+
if (!ends) {
|
|
449
|
+
ends = [null, null];
|
|
450
|
+
END_FRAMES.set(rule, ends);
|
|
451
|
+
}
|
|
452
|
+
let frame = ends[offset];
|
|
453
|
+
if (!frame) {
|
|
454
|
+
frame = [];
|
|
455
|
+
for (const f of rule.frames) {
|
|
456
|
+
if (f.offsets.includes(offset)) frame.push(...f.declarations);
|
|
457
|
+
}
|
|
458
|
+
ends[offset] = frame;
|
|
459
|
+
}
|
|
460
|
+
return frame;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const END_FRAMES = new WeakMap<
|
|
464
|
+
KeyframesRule,
|
|
465
|
+
[Declaration[] | null, Declaration[] | null]
|
|
466
|
+
>();
|
package/src/html/css/cascade.ts
CHANGED
|
@@ -30,7 +30,13 @@ import {
|
|
|
30
30
|
readIdent,
|
|
31
31
|
startsIdent,
|
|
32
32
|
} from './parse.js';
|
|
33
|
-
import type {
|
|
33
|
+
import type {
|
|
34
|
+
Declaration,
|
|
35
|
+
KeyframesRule,
|
|
36
|
+
StyleRule,
|
|
37
|
+
Stylesheet,
|
|
38
|
+
} from './parse.js';
|
|
39
|
+
import { NO_ANIMATIONS, restingFrames } from './animation.js';
|
|
34
40
|
import {
|
|
35
41
|
applyDeclaration,
|
|
36
42
|
blockify,
|
|
@@ -73,8 +79,11 @@ const enum Origin {
|
|
|
73
79
|
Presentation = 1,
|
|
74
80
|
Author = 2,
|
|
75
81
|
Inline = 3,
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
/** What an animation leaves (`restingFrames`): over every declaration
|
|
83
|
+
* but an `!important` one (CSS Cascade 5, 6.1). */
|
|
84
|
+
Animation = 4,
|
|
85
|
+
AuthorImportant = 5,
|
|
86
|
+
InlineImportant = 6,
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
interface Candidate {
|
|
@@ -1094,6 +1103,9 @@ export class Cascade {
|
|
|
1094
1103
|
readonly breakpoints: number[];
|
|
1095
1104
|
/** The counter styles the sheets define, over the predefined ones. */
|
|
1096
1105
|
readonly counterStyles: CounterStyles;
|
|
1106
|
+
/** The `@keyframes` rules, by name, in document order: which one a name
|
|
1107
|
+
* finds turns on the media in force (`keyframes`). */
|
|
1108
|
+
private _keyframes = new Map<string, KeyframesRule[]>();
|
|
1097
1109
|
|
|
1098
1110
|
/** A face's x-height, for `ex`, where the fonts can say. */
|
|
1099
1111
|
private _xHeightOf: FaceMetric | null;
|
|
@@ -1212,6 +1224,17 @@ export class Cascade {
|
|
|
1212
1224
|
this._hoverRules.note(selector);
|
|
1213
1225
|
this._focusRules.note(selector);
|
|
1214
1226
|
}
|
|
1227
|
+
for (const rule of sheet.keyframes ?? []) {
|
|
1228
|
+
let named = this._keyframes.get(rule.name);
|
|
1229
|
+
if (!named) this._keyframes.set(rule.name, (named = []));
|
|
1230
|
+
named.push(rule);
|
|
1231
|
+
for (const frame of rule.frames) {
|
|
1232
|
+
const declarations = frame.declarations;
|
|
1233
|
+
if (!this._vars && usesVars(declarations)) this._vars = true;
|
|
1234
|
+
this._noteViewportUnits(declarations);
|
|
1235
|
+
if (!this._lh && usesLh(declarations)) this._lh = true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1215
1238
|
for (const bp of sheet.breakpoints) breakpoints.add(bp);
|
|
1216
1239
|
// a query on the viewport's height reads it as a `vh` does
|
|
1217
1240
|
if (sheet.readsHeight) this.readsViewportHeight = true;
|
|
@@ -1400,6 +1423,42 @@ export class Cascade {
|
|
|
1400
1423
|
return band;
|
|
1401
1424
|
}
|
|
1402
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* The `@keyframes` an animation's name finds, or null: of the rules of
|
|
1428
|
+
* the name whose media hold, the one in the latest layer — no layer
|
|
1429
|
+
* outranking any, as for a style rule — and the last of those, but that
|
|
1430
|
+
* an `@-webkit-keyframes` never takes the place of an `@keyframes`, as in
|
|
1431
|
+
* Blink (`ScopedStyleResolver::AddKeyframeStyle`).
|
|
1432
|
+
*/
|
|
1433
|
+
keyframes(name: string): KeyframesRule | null {
|
|
1434
|
+
const named = this._keyframes.get(name);
|
|
1435
|
+
if (!named) return null;
|
|
1436
|
+
const width = this.viewportWidth / this.scale;
|
|
1437
|
+
const height = this.viewportHeight / this.scale;
|
|
1438
|
+
let found: KeyframesRule | null = null;
|
|
1439
|
+
for (const rule of named) {
|
|
1440
|
+
if (
|
|
1441
|
+
!mediaMatches(
|
|
1442
|
+
rule.media,
|
|
1443
|
+
width,
|
|
1444
|
+
this.look.colorScheme,
|
|
1445
|
+
height,
|
|
1446
|
+
this.scale,
|
|
1447
|
+
)
|
|
1448
|
+
) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
if (found) {
|
|
1452
|
+
const by = compareLayers(rule.layer, found.layer);
|
|
1453
|
+
if (by < 0 || (by === 0 && rule.prefixed && !found.prefixed)) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
found = rule;
|
|
1458
|
+
}
|
|
1459
|
+
return found;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1403
1462
|
/** Computed styles by sharing key, for one build (`sharedStyleFor`): one
|
|
1404
1463
|
* map for the elements shared without matching, one for the elements
|
|
1405
1464
|
* shared by what they matched. */
|
|
@@ -1840,7 +1899,14 @@ export class Cascade {
|
|
|
1840
1899
|
return line;
|
|
1841
1900
|
}
|
|
1842
1901
|
|
|
1843
|
-
/**
|
|
1902
|
+
/**
|
|
1903
|
+
* `styleFor`, from the rules and hints already gathered for `el`, and
|
|
1904
|
+
* what its animations leave on it (`restingFrames`). Which animations an
|
|
1905
|
+
* element has is the cascade's answer, so an element with one that
|
|
1906
|
+
* leaves a frame is styled twice: the second time with the frame's
|
|
1907
|
+
* declarations among the rest, at the animation origin, over the
|
|
1908
|
+
* author's normal ones and under their `!important` ones.
|
|
1909
|
+
*/
|
|
1844
1910
|
private _computeStyle(
|
|
1845
1911
|
el: Element,
|
|
1846
1912
|
parentStyle: ComputedStyle,
|
|
@@ -1849,6 +1915,41 @@ export class Cascade {
|
|
|
1849
1915
|
/** Whether the style is the element's own, and not that of one of its
|
|
1850
1916
|
* pseudo-elements. */
|
|
1851
1917
|
own = false,
|
|
1918
|
+
): ComputedStyle {
|
|
1919
|
+
let style = this._cascadeStyle(
|
|
1920
|
+
el,
|
|
1921
|
+
parentStyle,
|
|
1922
|
+
inFlexContainer,
|
|
1923
|
+
candidates,
|
|
1924
|
+
own,
|
|
1925
|
+
);
|
|
1926
|
+
if (style.animations !== NO_ANIMATIONS && this._keyframes.size) {
|
|
1927
|
+
const frames = restingFrames(style.animations, (name) =>
|
|
1928
|
+
this.keyframes(name),
|
|
1929
|
+
);
|
|
1930
|
+
if (frames) {
|
|
1931
|
+
style = this._cascadeStyle(
|
|
1932
|
+
el,
|
|
1933
|
+
parentStyle,
|
|
1934
|
+
inFlexContainer,
|
|
1935
|
+
withAnimations(candidates, frames),
|
|
1936
|
+
own,
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
// which faces of the document's own families this family, weight and
|
|
1941
|
+
// slant ask for — known only now, with all three computed
|
|
1942
|
+
this._families?.note(style);
|
|
1943
|
+
return style;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** The style the candidates come to, in their order. */
|
|
1947
|
+
private _cascadeStyle(
|
|
1948
|
+
el: Element,
|
|
1949
|
+
parentStyle: ComputedStyle,
|
|
1950
|
+
inFlexContainer: boolean,
|
|
1951
|
+
candidates: Candidate[],
|
|
1952
|
+
own: boolean,
|
|
1852
1953
|
): ComputedStyle {
|
|
1853
1954
|
const style = inherit(parentStyle, this.initial);
|
|
1854
1955
|
// custom properties first, in cascade order, so every `var()` in the
|
|
@@ -2004,9 +2105,6 @@ export class Cascade {
|
|
|
2004
2105
|
settleButton(style);
|
|
2005
2106
|
}
|
|
2006
2107
|
decorate(style);
|
|
2007
|
-
// which faces of the document's own families this family, weight and
|
|
2008
|
-
// slant ask for — known only now, with all three computed
|
|
2009
|
-
this._families?.note(style);
|
|
2010
2108
|
return style;
|
|
2011
2109
|
}
|
|
2012
2110
|
|
|
@@ -2885,6 +2983,31 @@ function pick(c: Candidate): Declaration[] {
|
|
|
2885
2983
|
return c.only < 0 ? c.declarations : [c.declarations[c.only]];
|
|
2886
2984
|
}
|
|
2887
2985
|
|
|
2986
|
+
/** Candidates in cascade order with what an element's animations leave
|
|
2987
|
+
* among them: after every normal declaration and before the first
|
|
2988
|
+
* `!important` one, a later animation after an earlier. */
|
|
2989
|
+
function withAnimations(
|
|
2990
|
+
candidates: readonly Candidate[],
|
|
2991
|
+
frames: readonly Declaration[][],
|
|
2992
|
+
): Candidate[] {
|
|
2993
|
+
let at = 0;
|
|
2994
|
+
while (at < candidates.length && candidates[at].origin < Origin.Animation) {
|
|
2995
|
+
at += 1;
|
|
2996
|
+
}
|
|
2997
|
+
return [
|
|
2998
|
+
...candidates.slice(0, at),
|
|
2999
|
+
...frames.map((declarations, order) => ({
|
|
3000
|
+
origin: Origin.Animation,
|
|
3001
|
+
layer: null,
|
|
3002
|
+
specificity: 0,
|
|
3003
|
+
order,
|
|
3004
|
+
declarations,
|
|
3005
|
+
only: -1,
|
|
3006
|
+
})),
|
|
3007
|
+
...candidates.slice(at),
|
|
3008
|
+
];
|
|
3009
|
+
}
|
|
3010
|
+
|
|
2888
3011
|
function pushRule(out: Candidate[], rule: StyleRule, origin: Origin): void {
|
|
2889
3012
|
let hasImportant = false;
|
|
2890
3013
|
for (const d of rule.declarations) {
|